Explorar o código

从老saas 迁移的 新代码

三七 hai 1 mes
pai
achega
b476e1098b
Modificáronse 100 ficheiros con 3551 adicións e 1268 borrados
  1. 7 9
      fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java
  2. 149 3
      fs-ai-call-task/src/main/java/com/fs/app/service/CallTaskService.java
  3. 23 0
      fs-ai-call-task/src/main/java/com/fs/app/task/Task.java
  4. 106 3
      fs-cid-workflow/src/main/java/com/fs/app/service/CidWorkflowTaskService.java
  5. 114 0
      fs-cid-workflow/src/main/java/com/fs/app/service/OutboundRetryTaskService.java
  6. 25 0
      fs-cid-workflow/src/main/java/com/fs/app/task/CidTask.java
  7. 14 0
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  8. 276 0
      fs-company/src/main/java/com/fs/company/controller/common/RecordingProxyController.java
  9. 9 16
      fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowServerController.java
  10. 2 4
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java
  11. 5 6
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java
  12. 167 21
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java
  13. 32 19
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java
  14. 2 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCalleesController.java
  15. 94 48
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  16. 2 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticWxController.java
  17. 7 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java
  18. 84 5
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java
  19. 3 4
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxClientController.java
  20. 4 18
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxDialogController.java
  21. 7 7
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxUserController.java
  22. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxUserGroupController.java
  23. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  24. 170 35
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  25. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  26. 3 2
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java
  27. 3 3
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java
  28. 2 1
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallVoiceTtsAliyunService.java
  29. 6 12
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java
  30. 13 12
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java
  31. 1 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java
  32. 1 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticBusiness.java
  33. 8 6
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java
  34. 70 11
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java
  35. 6 6
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java
  36. 1 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticWx.java
  37. 47 0
      fs-service/src/main/java/com/fs/company/domain/OutboundLineLimitLog.java
  38. 26 0
      fs-service/src/main/java/com/fs/company/dto/OutboundLimitResultVO.java
  39. 36 8
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java
  40. 10 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceCloneRefMapper.java
  41. 17 5
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  42. 9 8
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogSendmsgMapper.java
  43. 7 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallMapper.java
  44. 18 0
      fs-service/src/main/java/com/fs/company/mapper/OutboundLineLimitLogMapper.java
  45. 25 0
      fs-service/src/main/java/com/fs/company/param/AppendCustomersParam.java
  46. 5 3
      fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java
  47. 15 0
      fs-service/src/main/java/com/fs/company/service/ICompanyAiCallDataSyncService.java
  48. 9 8
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundBindService.java
  49. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  50. 10 9
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceApiService.java
  51. 10 9
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceBlacklistService.java
  52. 2 1
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCallerService.java
  53. 0 2
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneRefService.java
  54. 27 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneService.java
  55. 10 9
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceConfigService.java
  56. 3 3
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceLogsService.java
  57. 3 5
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogAddwxService.java
  58. 28 7
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java
  59. 10 9
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogSendmsgService.java
  60. 31 10
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  61. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyWorkflowService.java
  62. 7 7
      fs-service/src/main/java/com/fs/company/service/ICompanyWxAccountService.java
  63. 9 9
      fs-service/src/main/java/com/fs/company/service/ICompanyWxChatService.java
  64. 7 9
      fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java
  65. 7 7
      fs-service/src/main/java/com/fs/company/service/ICompanyWxDialogService.java
  66. 320 4
      fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java
  67. 9 1
      fs-service/src/main/java/com/fs/company/service/easycall/IEasyCallService.java
  68. 84 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyAiCallDataSyncServiceImpl.java
  69. 7 7
      fs-service/src/main/java/com/fs/company/service/impl/CompanyAiWorkflowServerServiceImpl.java
  70. 16 4
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneServiceImpl.java
  71. 0 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallBlacklistServiceImpl.java
  72. 8 14
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogAddwxServiceImpl.java
  73. 457 226
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  74. 15 16
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogSendmsgServiceImpl.java
  75. 3 4
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCalleesServiceImpl.java
  76. 429 350
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  77. 7 7
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticWxServiceImpl.java
  78. 5 5
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceServiceImpl.java
  79. 20 133
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  80. 25 6
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java
  81. 12 12
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxChatServiceImpl.java
  82. 1 6
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java
  83. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxDialogServiceImpl.java
  84. 14 17
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java
  85. 5 2
      fs-service/src/main/java/com/fs/company/service/impl/StatisticManageServiceImpl.java
  86. 87 2
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java
  87. 49 8
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java
  88. 7 7
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNode.java
  89. 52 16
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  90. 6 3
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java
  91. 0 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiSendMsgTaskNode.java
  92. 52 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/EasyCallTaskControlService.java
  93. 3 7
      fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java
  94. 1 6
      fs-service/src/main/java/com/fs/company/service/impl/call/node/StartNode.java
  95. 3 4
      fs-service/src/main/java/com/fs/company/service/impl/call/node/WorkflowNodeFactory.java
  96. 9 2
      fs-service/src/main/java/com/fs/company/vo/AiCallWorkflowConditionVo.java
  97. 2 0
      fs-service/src/main/java/com/fs/company/vo/CallContentVO.java
  98. 5 5
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallBlacklistCheckVO.java
  99. 3 3
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogAddwxVO.java
  100. 23 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO.java

+ 7 - 9
fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java

@@ -164,10 +164,7 @@ public class CompanyVoiceRoboticController extends BaseController
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
-        for (Long id : ids) {
-            companyVoiceRoboticService.updateDelFlag(id, 1);
-        }
-        return AjaxResult.success();
+        return toAjax(companyVoiceRoboticService.deleteCompanyVoiceRoboticByIds(ids));
     }
     /**
      * 删除机器人外呼任务
@@ -182,16 +179,16 @@ public class CompanyVoiceRoboticController extends BaseController
      * 查询状态
      */
     @GetMapping("/statusList")
-    public R statusList(String ids, Long companyId){
+    public R statusList(String ids,Long companyId){
         List<String> list = Arrays.stream(ids.split(",")).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
-        return R.ok().put("data", list.stream().map(e -> aiCallService.queryCallTaskInfo(TaskInfo.builder().taskID(e).build(), companyId)).collect(Collectors.toMap(QueryCallTaskInfoResult::getTaskID, e -> e)));
+        return R.ok().put("data", list.stream().map(e -> aiCallService.queryCallTaskInfo(TaskInfo.builder().taskID(e).build(),companyId)).collect(Collectors.toMap(QueryCallTaskInfoResult::getTaskID, e -> e)));
     }
     /**
      * 启动任务
      */
 	@GetMapping("/startRobotic")
     public R startRobotic(String taskId, Long companyId){
-        aiCallService.startCallTask(TaskInfo.builder().taskID(taskId).build(), companyId);
+        aiCallService.startCallTask(TaskInfo.builder().taskID(taskId).build(),companyId);
         return R.ok();
     }
     /**
@@ -199,7 +196,7 @@ public class CompanyVoiceRoboticController extends BaseController
      */
 	@GetMapping("/stopRobotic")
     public R stopRobotic(String taskId, Long companyId){
-        aiCallService.stopCallTask(TaskInfo.builder().taskID(taskId).build(), companyId);
+        aiCallService.stopCallTask(TaskInfo.builder().taskID(taskId).build(),companyId);
         return R.ok();
     }
     /**
@@ -227,10 +224,11 @@ public class CompanyVoiceRoboticController extends BaseController
         companyVoiceRoboticService.dispenseWx(id);
         return R.ok();
     }
+
     /**
      * 启动任务
      */
-	@GetMapping("/taskRun")
+    @GetMapping("/taskRun")
     public R taskRun(Long id){
         companyVoiceRoboticService.taskRun(id);
         return R.ok();

+ 149 - 3
fs-ai-call-task/src/main/java/com/fs/app/service/CallTaskService.java

@@ -1,9 +1,14 @@
 package com.fs.app.service;
 
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.core.redis.RedisCacheT;
+import com.fs.company.domain.CompanyVoiceRobotic;
+import com.fs.company.mapper.CompanyVoiceRoboticMapper;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.param.PauseRoboticActiveParam;
 import com.fs.company.service.*;
 import com.fs.company.service.impl.call.node.AiCallTaskNode;
 import com.fs.course.config.RedisKeyScanner;
@@ -11,9 +16,12 @@ import lombok.AllArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalTime;
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -30,6 +38,8 @@ public class CallTaskService {
 
     private final RedisCache redisCache2;
     private final CompanyWorkflowEngine companyWorkflowEngine;
+    private final ICompanyVoiceRoboticService companyVoiceRoboticService;
+    private final CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
     private final ExecutorService cidExcutor = new ThreadPoolExecutor(
             32,
             64,
@@ -39,6 +49,11 @@ public class CallTaskService {
             r -> new Thread(r, "callPool-" + System.currentTimeMillis()),
             new ThreadPoolExecutor.CallerRunsPolicy()
     );
+
+    @Autowired
+    @Qualifier("cidWorkFlowExecutor")
+    private Executor cidWorkFlowExecutor;
+
     private final RedisKeyScanner redisKeyScanner;
 
     /**
@@ -49,25 +64,156 @@ public class CallTaskService {
         String delayCallKeyPrefix = AiCallTaskNode.getDelayCallKeyPrefix(cidGroupNo,null) + "*";
         Set<String> keys = redisKeyScanner.scanMatchKey(delayCallKeyPrefix);
         log.info("共扫描到 {} 个待处理键", keys.size());
+
+        // 本地缓存已查询的任务暂停状态,避免同一批次重复查询
+        Map<Long, Boolean> pausedCache = new ConcurrentHashMap<>();
         keys.parallelStream().forEach(key -> {
             try {
                 //doExec
                 CompletableFuture.runAsync(() -> {
                     try {
                         ExecutionContext context = redisCache2.getCacheObject(key);
+                        if (context == null) {
+                            log.warn("工作流延时任务context为空,跳过 - key: {}", key);
+                            redisCache2.deleteObject(key);
+                            return;
+                        }
+                        // 任务暂停守卫检查(roboticId即CompanyVoiceRobotic.id,是实际暂停操作的目标)
+                        Long taskId = context.getVariable("roboticId", Long.class);
+                        if (taskId != null && pausedCache.computeIfAbsent(taskId, id -> companyVoiceRoboticService.isTaskPaused(id))) {
+                            // 延时key是时间分片前缀,下一分钟就不会再扫到,直接删除
+                            // 同步context信息到DB exec,供恢复时resumePausedInstances使用
+                            context.setVariable("callSource", "callTaskTimer");
+                            context.setVariable("_delayTargetNodeKey", context.getCurrentNodeKey());
+                            companyWorkflowEngine.updateExecVariables(context.getWorkflowInstanceId(), context.getVariables());
+                            log.info("任务已暂停,删除延时key并同步exec,等待恢复时从DB重建 - taskId: {}, key: {}", taskId, key);
+                            redisCache2.deleteObject(key);
+                            return;
+                        }
                         context.setVariable("callRedisKey", key);
                         context.setVariable("callSource", "callTaskTimer");
                         companyWorkflowEngine.timeDoExecute(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context.getVariables());
+                        redisCache2.deleteObject(key);
                     } catch (Exception e) {
                         log.error("处理工作流延时任务异常 - key: {}", key, e);
                     }
-                }, cidExcutor).thenRun(()->{
-                    redisCache2.deleteObject(key);
-                });
+                }, cidWorkFlowExecutor);
             } catch (Exception ex) {
                 log.error("处理工作流延时任务异常 - key: {}", key, ex);
             }
         });
         log.info("===========工作流延时任务扫描结束===========");
     }
+
+
+    /**
+     * 检查任务状态来暂停外呼任务的外呼运行
+     */
+    public void taskStatusChange(){
+        LocalTime now = LocalTime.now();
+        log.info("===========任务状态变更扫描开始, 当前时间: {}, 分组: {}===========", now, cidGroupNo);
+        pauseOutOfRangeTasks(now);
+        resumeInRangeTasks(now);
+        log.info("===========任务状态变更扫描结束===========");
+    }
+
+    private void pauseOutOfRangeTasks(LocalTime now) {
+        List<CompanyVoiceRobotic> activeTasks = companyVoiceRoboticMapper.selectList(
+                new QueryWrapper<CompanyVoiceRobotic>()
+                        .eq("task_status", 1)
+                        .eq("task_type", 1)
+                        .eq("cid_group_no", cidGroupNo)
+                        .eq("del_flag", 0)
+        );
+        log.info("扫描到执行中的普通任务 {} 个", activeTasks.size());
+        if (activeTasks.isEmpty()) {
+            return;
+        }
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        for (CompanyVoiceRobotic task : activeTasks) {
+            if (isOutOfRuntimeRange(task, now)) {
+                log.info("任务[{}]运行时间范围[{},{}),当前{}超出范围,准备自动暂停", task.getId(), task.getRuntimeRangeStart(), task.getRuntimeRangeEnd(), now);
+                futures.add(CompletableFuture.runAsync(() -> {
+                    try {
+                        PauseRoboticActiveParam param = new PauseRoboticActiveParam();
+                        param.setTaskId(task.getId());
+                        param.setActiveType(1);
+                        companyVoiceRoboticService.pauseRoboticActive(param);
+                        companyVoiceRoboticMapper.update(null,
+                                new LambdaUpdateWrapper<CompanyVoiceRobotic>()
+                                        .eq(CompanyVoiceRobotic::getId, task.getId())
+                                        .set(CompanyVoiceRobotic::getPauseSource, "auto")
+                        );
+                        log.info("任务[{}]超出运行时间范围,已自动暂停", task.getId());
+                    } catch (Exception e) {
+                        log.error("自动暂停任务[{}]异常", task.getId(), e);
+                    }
+                }, cidWorkFlowExecutor));
+            }
+        }
+        if (!futures.isEmpty()) {
+            log.info("本次需自动暂停 {} 个任务,等待并行执行完成", futures.size());
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        }
+    }
+
+    private void resumeInRangeTasks(LocalTime now) {
+        List<CompanyVoiceRobotic> pausedTasks = companyVoiceRoboticMapper.selectList(
+                new QueryWrapper<CompanyVoiceRobotic>()
+                        .eq("task_status", 2)
+                        .eq("task_type", 1)
+                        .eq("pause_source", "auto")
+                        .eq("cid_group_no", cidGroupNo)
+                        .eq("del_flag", 0)
+        );
+        log.info("扫描到自动暂停中的普通任务 {} 个", pausedTasks.size());
+        if (pausedTasks.isEmpty()) {
+            return;
+        }
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        for (CompanyVoiceRobotic task : pausedTasks) {
+            if (isWithinRuntimeRange(task, now)) {
+                log.info("任务[{}]运行时间范围[{},{}),当前{}在范围内,准备自动恢复", task.getId(), task.getRuntimeRangeStart(), task.getRuntimeRangeEnd(), now);
+                futures.add(CompletableFuture.runAsync(() -> {
+                    try {
+                        PauseRoboticActiveParam param = new PauseRoboticActiveParam();
+                        param.setTaskId(task.getId());
+                        param.setActiveType(2);
+                        companyVoiceRoboticService.pauseRoboticActive(param);
+                        companyVoiceRoboticMapper.update(null,
+                                new LambdaUpdateWrapper<CompanyVoiceRobotic>()
+                                        .eq(CompanyVoiceRobotic::getId, task.getId())
+                                        .set(CompanyVoiceRobotic::getPauseSource, null)
+                        );
+                        log.info("任务[{}]回到运行时间范围,已自动恢复", task.getId());
+                    } catch (Exception e) {
+                        log.error("自动恢复任务[{}]异常", task.getId(), e);
+                    }
+                }, cidWorkFlowExecutor));
+            }
+        }
+        if (!futures.isEmpty()) {
+            log.info("本次需自动恢复 {} 个任务,等待并行执行完成", futures.size());
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        }
+    }
+
+    private boolean isOutOfRuntimeRange(CompanyVoiceRobotic task, LocalTime now) {
+        LocalTime start = task.getRuntimeRangeStart();
+        LocalTime end = task.getRuntimeRangeEnd();
+        if (start == null || end == null) {
+            return false;
+        }
+        if (start.isBefore(end)) {
+            return now.isBefore(start) || now.isAfter(end);
+        } else {
+            return now.isAfter(end) && now.isBefore(start);
+        }
+    }
+
+    private boolean isWithinRuntimeRange(CompanyVoiceRobotic task, LocalTime now) {
+        return !isOutOfRuntimeRange(task, now);
+    }
+
+
 }

+ 23 - 0
fs-ai-call-task/src/main/java/com/fs/app/task/Task.java

@@ -38,8 +38,31 @@ public class Task {
 //    }
 
 
+//    @Scheduled(cron = "0/30 * * * * ?")
+//    public void cidWorkflowRun(){
+//        taskService.cidWorkflowCallRun();
+//    }
+
     @Scheduled(cron = "0/30 * * * * ?")
     public void cidWorkflowRun(){
+
+        if (taskService == null) {
+            log.error("[Task] taskService 未注入,请检查 Spring 配置");
+            return;
+        }
         taskService.cidWorkflowCallRun();
     }
+
+
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void taskStatusChange(){
+
+        if (taskService == null) {
+            log.error("[Task] taskService 未注入,请检查 Spring 配置");
+            return;
+        }
+
+            taskService.taskStatusChange();
+        }
+
 }

+ 106 - 3
fs-cid-workflow/src/main/java/com/fs/app/service/CidWorkflowTaskService.java

@@ -1,5 +1,7 @@
 package com.fs.app.service;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
@@ -29,6 +31,7 @@ public class CidWorkflowTaskService {
     Integer cidGroupNo;
     private final CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
     private final CompanyWorkflowEngine companyWorkflowEngine;
+    private final ICompanyVoiceRoboticService companyVoiceRoboticService;
 
     private final RedisKeyScanner redisKeyScanner;
 
@@ -41,15 +44,51 @@ public class CidWorkflowTaskService {
     @Autowired
     private WorkflowNodeFactory nodeFactory;
 
+    /** 认领态(RUNNING)超时分钟数,超过该时间未流转的任务视为卡死并回扫重置为失败 */
+    @Value("${cid-workflow.running-timeout-minutes:30}")
+    Integer runningTimeoutMinutes;
+
     /**
      * 扫描当前分组下就绪任务,并开启执行
      */
     public void runCidWorkflow() {
+//        List<CompanyAiWorkflowExec> companyAiWorkflowExecs = companyAiWorkflowExecMapper.selectExecListWithTimeAvailableByStatusAndGroupNo(ExecutionStatusEnum.READY.getValue(), cidGroupNo);
+//        System.out.println(companyAiWorkflowExecs);
+//        if (null != companyAiWorkflowExecs && companyAiWorkflowExecs.size() > 0) {
+//            companyAiWorkflowExecs.parallelStream().forEach(exec -> {
+//                try {
+//                    companyWorkflowEngine.executeNode(exec.getWorkflowInstanceId(), exec.getCurrentNodeKey());
+//                } catch (Exception e) {
+//                    log.error("处理就绪任务异常 - exec: {}", exec, e);
+//                }
+//            });
+//        }
+
         List<CompanyAiWorkflowExec> companyAiWorkflowExecs = companyAiWorkflowExecMapper.selectExecListWithTimeAvailableByStatusAndGroupNo(ExecutionStatusEnum.READY.getValue(), cidGroupNo);
-        System.out.println(companyAiWorkflowExecs);
         if (null != companyAiWorkflowExecs && companyAiWorkflowExecs.size() > 0) {
-            companyAiWorkflowExecs.parallelStream().forEach(exec -> {
+            // 本地缓存已查询的任务暂停状态,避免同一批次重复查询
+            Map<Long, Boolean> pausedCache = new HashMap<>();
+            companyAiWorkflowExecs.forEach(exec -> {
                 try {
+                    // 任务暂停守卫检查(从 variables JSON 中提取 roboticId,即 CompanyVoiceRobotic.id)
+                    Long taskId = extractRoboticIdFromExec(exec);
+                    if (taskId != null) {
+                        boolean paused = pausedCache.computeIfAbsent(taskId, id -> companyVoiceRoboticService.isTaskPaused(id));
+                        if (paused) {
+                            log.debug("任务已暂停,跳过执行 - taskId: {}, execId: {}", taskId, exec.getId());
+                            return;
+                        }
+                    }
+                    // 原子认领:READY -> RUNNING,仅认领成功(影响行数=1)才提交执行,
+                    // 防止上一轮异步任务尚未完成时下一次定时扫描重复调度同一条记录
+                    int claimed = companyAiWorkflowExecMapper.claimExecForRun(
+                            exec.getId(),
+                            ExecutionStatusEnum.READY.getValue(),
+                            ExecutionStatusEnum.RUNNING.getValue());
+                    if (claimed == 0) {
+                        log.debug("任务已被认领,跳过重复执行 - execId: {}", exec.getId());
+                        return;
+                    }
                     companyWorkflowEngine.executeNode(exec.getWorkflowInstanceId(), exec.getCurrentNodeKey());
                 } catch (Exception e) {
                     log.error("处理就绪任务异常 - exec: {}", exec, e);
@@ -62,10 +101,41 @@ public class CidWorkflowTaskService {
      * 扫描可执行任务,并激活执行
      */
     public void activateTimeAvailableTask() {
+//        List<CompanyAiWorkflowExec> companyAiWorkflowExecs = companyAiWorkflowExecMapper.selectExecListWithTimeAvailableByStatusAndGroupNo(ExecutionStatusEnum.PENDING.getValue(), cidGroupNo);
+//        if (null != companyAiWorkflowExecs && companyAiWorkflowExecs.size() > 0) {
+//            companyAiWorkflowExecs.parallelStream().forEach(exec -> {
+//                try {
+//                    companyWorkflowEngine.executeNode(exec.getWorkflowInstanceId(), exec.getCurrentNodeKey());
+//                } catch (Exception e) {
+//                    log.error("处理就绪任务异常 - exec: {}", exec, e);
+//                }
+//            });
+//        }
         List<CompanyAiWorkflowExec> companyAiWorkflowExecs = companyAiWorkflowExecMapper.selectExecListWithTimeAvailableByStatusAndGroupNo(ExecutionStatusEnum.PENDING.getValue(), cidGroupNo);
         if (null != companyAiWorkflowExecs && companyAiWorkflowExecs.size() > 0) {
-            companyAiWorkflowExecs.parallelStream().forEach(exec -> {
+            // 本地缓存已查询的任务暂停状态,避免同一批次重复查询
+            Map<Long, Boolean> pausedCache = new HashMap<>();
+            companyAiWorkflowExecs.forEach(exec -> {
                 try {
+                    // 任务暂停守卫检查(从 variables JSON 中提取 roboticId,即 CompanyVoiceRobotic.id)
+                    Long taskId = extractRoboticIdFromExec(exec);
+                    if (taskId != null) {
+                        boolean paused = pausedCache.computeIfAbsent(taskId, id -> companyVoiceRoboticService.isTaskPaused(id));
+                        if (paused) {
+                            log.debug("任务已暂停,跳过执行 - taskId: {}, execId: {}", taskId, exec.getId());
+                            return;
+                        }
+                    }
+                    // 原子认领:PENDING -> RUNNING,仅认领成功(影响行数=1)才提交执行,
+                    // 防止上一轮异步任务尚未完成时下一次定时扫描重复激活同一条记录
+                    int claimed = companyAiWorkflowExecMapper.claimExecForRun(
+                            exec.getId(),
+                            ExecutionStatusEnum.PENDING.getValue(),
+                            ExecutionStatusEnum.RUNNING.getValue());
+                    if (claimed == 0) {
+                        log.debug("任务已被认领,跳过重复执行 - execId: {}", exec.getId());
+                        return;
+                    }
                     companyWorkflowEngine.executeNode(exec.getWorkflowInstanceId(), exec.getCurrentNodeKey());
                 } catch (Exception e) {
                     log.error("处理就绪任务异常 - exec: {}", exec, e);
@@ -108,4 +178,37 @@ public class CidWorkflowTaskService {
         });
     }
 
+    /**
+     * 从 exec 记录的 variables JSON 中提取 roboticId(CompanyVoiceRobotic.id)
+     * 用于暂停守卫检查,fallback 到 exec.getBusinessKey()
+     */
+    private Long extractRoboticIdFromExec(CompanyAiWorkflowExec exec) {
+        if (StringUtils.isNotBlank(exec.getVariables())) {
+            try {
+                JSONObject vars = JSONObject.parseObject(exec.getVariables());
+                if (vars != null && vars.containsKey("roboticId")) {
+                    return vars.getLong("roboticId");
+                }
+            } catch (Exception e) {
+                log.warn("extractRoboticIdFromExec解析variables失败 - execId: {}", exec.getId(), e);
+            }
+        }
+        return exec.getBusinessKey();
+    }
+
+    /**
+     * 回扫超时认领任务:将处于 RUNNING 态且超过 runningTimeoutMinutes 分钟未更新的记录重置为 FAILURE。
+     * 用于兜底进程重启 / 线程池拒绝导致任务永久卡在认领态(RUNNING)无法被再次扫描的情况。
+     */
+    public void resetTimeoutRunningTask() {
+        java.time.LocalDateTime timeoutTime = java.time.LocalDateTime.now().minusMinutes(runningTimeoutMinutes);
+        int reset = companyAiWorkflowExecMapper.resetTimeoutRunningExec(
+                cidGroupNo,
+                ExecutionStatusEnum.RUNNING.getValue(),
+                ExecutionStatusEnum.FAILURE.getValue(),
+                timeoutTime);
+        if (reset > 0) {
+            log.warn("回扫超时认领任务,重置为失败状态 - groupNo: {}, count: {}, timeoutMinutes: {}", cidGroupNo, reset, runningTimeoutMinutes);
+        }
+    }
 }

+ 114 - 0
fs-cid-workflow/src/main/java/com/fs/app/service/OutboundRetryTaskService.java

@@ -0,0 +1,114 @@
+package com.fs.app.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.vo.easycall.EasyCallCommonAddCallListParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Set;
+
+/**
+ * 外呼限制重试任务服务(ZSET 方案)
+ * <p>
+ * 使用 Redis Sorted Set 存储待重试任务,score 为 nextAvailableTime 毫秒时间戳。
+ * 定时任务通过 ZRANGEBYSCORE 按批次查询已到期任务,支撑百万级数据量。
+ */
+@Service
+@Slf4j
+public class OutboundRetryTaskService {
+
+    private static final String ZSET_KEY = "outbound:limit:retry:zset";
+    private static final int BATCH_SIZE = 1000;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private IEasyCallService easyCallService;
+
+    /**
+     * 处理外呼重试任务(ZSET 方案)
+     * <p>
+     * 1. 用 ZRANGEBYSCORE 查询 score <= 当前时间 的任务(即已到期的)
+     * 2. 每次取一批(100 条),循环处理直到没有到期任务为止
+     * 3. 处理完后 ZREM 移除
+     */
+    public void processRetryTasks() {
+        int totalProcessed = 0;
+
+        try {
+            while (true) {
+                long now = System.currentTimeMillis();
+                // 使用 ZRANGEBYSCORE 查询已到期的任务(score <= now),每批最多 BATCH_SIZE 条
+                Set<String> readyTasks = redisCache.zRangeByScore(ZSET_KEY, 0, now, 0, BATCH_SIZE);
+                if (readyTasks == null || readyTasks.isEmpty()) {
+                    break;
+                }
+
+                for (String taskJson : readyTasks) {
+                    try {
+                        processOneRetryTask(taskJson);
+                    } catch (Exception e) {
+                        log.error("processRetryTasks: 处理重试任务异常", e);
+                        // 处理失败也要移除,避免死循环
+                        redisCache.zSetRemove(ZSET_KEY, taskJson);
+                    }
+                    totalProcessed++;
+                }
+            }
+        } catch (Exception e) {
+            log.error("processRetryTasks: 扫描重试任务异常", e);
+        }
+
+        if (totalProcessed > 0) {
+            log.info("processRetryTasks: 本次共处理 {} 个重试任务", totalProcessed);
+        }
+    }
+
+    /**
+     * 处理单个重试任务
+     */
+    private void processOneRetryTask(String taskJson) {
+        JSONObject retryData = JSON.parseObject(taskJson);
+        if (retryData == null) {
+            redisCache.zSetRemove(ZSET_KEY, taskJson);
+            return;
+        }
+
+        Long companyId = retryData.getLong("companyId");
+        Long gatewayId = retryData.getLong("gatewayId");
+        EasyCallCommonAddCallListParam param = retryData.getObject("param", EasyCallCommonAddCallListParam.class);
+
+        if (companyId == null || gatewayId == null || param == null) {
+            log.warn("processOneRetryTask: 参数不完整,移除无效任务");
+            redisCache.zSetRemove(ZSET_KEY, taskJson);
+            return;
+        }
+
+        // 从 param 中获取 batchId(正常流程中 addCommonCallList 前已通过 setBatchId 设置)
+        Long batchId = param.getBatchId();
+
+        log.info("processOneRetryTask: 开始重试外呼 - companyId: {}, gatewayId: {}", companyId, gatewayId);
+
+        // 重新调用 addCommonCallList(内部会再次检查限制,如果仍被限制会再次存入 ZSET)
+        boolean success = easyCallService.addCommonCallList(param, companyId, gatewayId);
+        if (success) {
+            log.info("processOneRetryTask: 重试成功 - companyId: {}, gatewayId: {}", companyId, gatewayId);
+            // 启动外呼任务
+            if (batchId != null) {
+                easyCallService.startTask(batchId, null);
+            } else {
+                log.warn("processOneRetryTask: batchId 为空,跳过启动任务 - companyId: {}, gatewayId: {}", companyId, gatewayId);
+            }
+        } else {
+            log.warn("processOneRetryTask: 重试未成功(可能仍被限制,已重新入队)- companyId: {}, gatewayId: {}", companyId, gatewayId);
+        }
+
+        // 无论成功失败都移除当前记录(如果仍被限制,addCommonCallList 内部会重新 saveRetryTask)
+        redisCache.zSetRemove(ZSET_KEY, taskJson);
+    }
+}

+ 25 - 0
fs-cid-workflow/src/main/java/com/fs/app/task/CidTask.java

@@ -1,6 +1,7 @@
 package com.fs.app.task;
 
 import com.fs.app.service.CidWorkflowTaskService;
+import com.fs.app.service.OutboundRetryTaskService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -23,6 +24,9 @@ public class CidTask {
     @Autowired
     private CidWorkflowTaskService cidWorkflowTaskService;
 
+    @Autowired
+    private OutboundRetryTaskService outboundRetryTaskService;
+
 
     /**
      * 扫描当前分组下就绪任务,并开启执行
@@ -48,7 +52,28 @@ public class CidTask {
 //        cidWorkflowTaskService.runContinueTask();
 //    }
 
+    /**
+     * 回扫超时认领任务 - 每5分钟执行一次
+     * 将卡在认领态(RUNNING)且长时间未流转的任务重置为失败,兜底进程重启/线程池拒绝导致的永久卡死
+     */
+    @Scheduled(cron = "0 0/5 * * * ?")
+    public void resetTimeoutRunningTask() {
+
+            cidWorkflowTaskService.resetTimeoutRunningTask();
+
+    }
+
+    /**
+     * 外呼重试任务 - 每30分钟执行一次
+     * 扫描 Redis 中被外呼限制拦截的待重试呼叫,到达 nextAvailableTime 后重新执行
+     */
+    @Scheduled(cron = "0 0/30 * * * ?")
+    public void retryOutboundCallTask() {
+        log.info("执行外呼限制重试机制");
+
+        outboundRetryTaskService.processRetryTasks();
 
+    }
 
 
 }

+ 14 - 0
fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java

@@ -344,6 +344,20 @@ public class RedisCache
         return redisTemplate.opsForZSet().reverseRange(key, start, end);
     }
 
+    /**
+     * 按 score 范围查询有序集合元素(带分页)
+     *
+     * @param key    有序集合键
+     * @param min    最小 score(包含)
+     * @param max    最大 score(包含)
+     * @param offset 偏移量
+     * @param count  数量限制
+     * @return 元素集合
+     */
+    public Set<String> zRangeByScore(String key, double min, double max, long offset, long count) {
+        return redisTemplate.opsForZSet().rangeByScore(key, min, max, offset, count);
+    }
+
     /**
      * 获取有序集合元素数量
      *

+ 276 - 0
fs-company/src/main/java/com/fs/company/controller/common/RecordingProxyController.java

@@ -0,0 +1,276 @@
+package com.fs.company.controller.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 录音文件代理接口
+ * 解决 HTTPS 页面下无法直接访问 HTTP 录音资源的问题
+ */
+@RestController
+public class RecordingProxyController {
+
+    private static final Logger log = LoggerFactory.getLogger(RecordingProxyController.class);
+
+    /**
+     * 允许代理的录音服务器地址白名单
+     */
+    private static final List<String> ALLOWED_HOSTS = Arrays.asList(
+            "129.28.164.235:8899"
+    );
+
+    /**
+     * 允许代理的路径前缀
+     */
+    private static final String ALLOWED_PATH_PREFIX = "/recordings/";
+
+    /**
+     * 流式缓冲区大小
+     */
+    private static final int BUFFER_SIZE = 4096;
+
+    /**
+     * 连接超时时间(毫秒)
+     */
+    private static final int CONNECT_TIMEOUT = 5000;
+
+    /**
+     * 读取超时时间(毫秒)
+     */
+    private static final int READ_TIMEOUT = 30000;
+
+    /**
+     * 录音文件代理接口
+     * 前端通过此接口请求录音文件,后端转发到录音服务器获取文件流
+     *
+     * @param url      录音文件的原始 HTTP 地址
+     * @param request  HTTP 请求
+     * @param response HTTP 响应
+     */
+    @GetMapping("/common/proxy/recording")
+    public void proxyRecording(@RequestParam("url") String url,
+                               HttpServletRequest request,
+                               HttpServletResponse response) {
+        // 1. 安全校验
+        if (!isUrlAllowed(url)) {
+            log.warn("录音代理请求被拒绝,非法地址: {}", url);
+            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+            try {
+                response.getWriter().write("Access denied: URL not allowed");
+            } catch (IOException e) {
+                log.error("写入错误响应失败", e);
+            }
+            return;
+        }
+
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // 2. 建立到录音服务器的连接
+            URL targetUrl = new URL(url);
+            connection = (HttpURLConnection) targetUrl.openConnection();
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(CONNECT_TIMEOUT);
+            connection.setReadTimeout(READ_TIMEOUT);
+            connection.setInstanceFollowRedirects(true);
+
+            // 3. 转发 Range 请求头(支持音频播放器拖动进度条)
+            String rangeHeader = request.getHeader("Range");
+            if (rangeHeader != null && !rangeHeader.isEmpty()) {
+                connection.setRequestProperty("Range", rangeHeader);
+            }
+
+            // 4. 发起请求
+            int responseCode = connection.getResponseCode();
+
+            // 5. 处理错误响应
+            if (responseCode >= 400) {
+                log.error("录音服务器返回错误状态码: {}, url: {}", responseCode, url);
+                response.setStatus(responseCode);
+                return;
+            }
+
+            // 6. 设置响应状态码(200 或 206)
+            response.setStatus(responseCode);
+
+            // 7. 设置 Content-Type
+            String contentType = connection.getContentType();
+            if (contentType != null && !contentType.isEmpty()) {
+                response.setContentType(contentType);
+            } else {
+                // 根据文件扩展名推断 Content-Type
+                response.setContentType(guessContentType(url));
+            }
+
+            // 7.1 设置 Content-Disposition,使浏览器下载时保存为正确的文件名
+            String fileName = extractFileName(url);
+            if (fileName != null) {
+                response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
+            }
+
+            // 8. 转发关键响应头
+            String contentLength = connection.getHeaderField("Content-Length");
+            if (contentLength != null) {
+                response.setHeader("Content-Length", contentLength);
+            }
+
+            String contentRange = connection.getHeaderField("Content-Range");
+            if (contentRange != null) {
+                response.setHeader("Content-Range", contentRange);
+            }
+
+            String acceptRanges = connection.getHeaderField("Accept-Ranges");
+            if (acceptRanges != null) {
+                response.setHeader("Accept-Ranges", acceptRanges);
+            } else {
+                response.setHeader("Accept-Ranges", "bytes");
+            }
+
+            // 9. 流式传输文件内容
+            inputStream = connection.getInputStream();
+            outputStream = response.getOutputStream();
+            byte[] buffer = new byte[BUFFER_SIZE];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+            outputStream.flush();
+
+        } catch (IOException e) {
+            log.error("录音文件代理请求失败, url: {}", url, e);
+            if (!response.isCommitted()) {
+                response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
+            }
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭输入流失败", e);
+                }
+            }
+            if (outputStream != null) {
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭输出流失败", e);
+                }
+            }
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 校验 URL 是否在允许的白名单范围内
+     *
+     * @param url 待校验的 URL
+     * @return 是否允许代理
+     */
+    private boolean isUrlAllowed(String url) {
+        if (url == null || url.isEmpty()) {
+            return false;
+        }
+
+        // 必须是 http 协议
+        if (!url.startsWith("http://") && !url.startsWith("https://")) {
+            return false;
+        }
+
+        try {
+            URL parsedUrl = new URL(url);
+            String host = parsedUrl.getHost();
+            int port = parsedUrl.getPort();
+            String hostWithPort = port > 0 ? host + ":" + port : host;
+            String path = parsedUrl.getPath();
+
+            // 校验主机地址是否在白名单内
+            boolean hostAllowed = ALLOWED_HOSTS.contains(hostWithPort);
+
+            // 校验路径是否包含允许的前缀
+            boolean pathAllowed = path != null && path.startsWith(ALLOWED_PATH_PREFIX);
+
+            return hostAllowed && pathAllowed;
+        } catch (Exception e) {
+            log.warn("URL 解析失败: {}", url, e);
+            return false;
+        }
+    }
+
+    /**
+     * 根据文件扩展名推断 Content-Type
+     *
+     * @param url 文件 URL
+     * @return Content-Type
+     */
+    private String guessContentType(String url) {
+        if (url == null) {
+            return "application/octet-stream";
+        }
+        String lowerUrl = url.toLowerCase();
+        if (lowerUrl.contains(".wav")) {
+            return "audio/wav";
+        } else if (lowerUrl.contains(".mp3")) {
+            return "audio/mpeg";
+        } else if (lowerUrl.contains(".ogg")) {
+            return "audio/ogg";
+        } else if (lowerUrl.contains(".flac")) {
+            return "audio/flac";
+        } else if (lowerUrl.contains(".m4a") || lowerUrl.contains(".aac")) {
+            return "audio/aac";
+        }
+        return "application/octet-stream";
+    }
+
+    /**
+     * 从录音文件 URL 中提取文件名
+     * 支持直接路径 /recordings/xxx.wav 和 query 参数 ?filename=xxx.wav
+     *
+     * @param url 录音文件 URL
+     * @return 文件名,无法提取时返回 null
+     */
+    private String extractFileName(String url) {
+        if (url == null || url.isEmpty()) {
+            return null;
+        }
+        try {
+            // 先尝试从 query 参数 filename 获取
+            URL parsedUrl = new URL(url);
+            String query = parsedUrl.getQuery();
+            if (query != null && query.contains("filename=")) {
+                for (String param : query.split("&")) {
+                    if (param.startsWith("filename=")) {
+                        return param.substring("filename=".length());
+                    }
+                }
+            }
+            // 从路径中提取文件名
+            String path = parsedUrl.getPath();
+            if (path != null && path.contains("/")) {
+                String name = path.substring(path.lastIndexOf('/') + 1);
+                if (!name.isEmpty()) {
+                    return name;
+                }
+            }
+        } catch (Exception e) {
+            log.debug("提取文件名失败: {}", url);
+        }
+        return null;
+    }
+}

+ 9 - 16
fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowServerController.java

@@ -1,27 +1,20 @@
 package com.fs.company.controller.company;
 
-import java.util.List;
-
-import com.fs.common.core.domain.R;
-import com.fs.company.param.BindCidServerParam;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
 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.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyAiWorkflowServer;
+import com.fs.company.param.BindCidServerParam;
 import com.fs.company.service.ICompanyAiWorkflowServerService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
 
 /**
  * cid服务Controller

+ 2 - 4
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java

@@ -7,9 +7,7 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.company.domain.CompanyVoiceBlacklist;
 import com.fs.company.domain.CompanyVoiceRoboticCallBlacklist;
-import com.fs.company.service.ICompanyVoiceBlacklistService;
 import com.fs.company.service.ICompanyVoiceRoboticCallBlacklistService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -89,7 +87,7 @@ public class CompanyVoiceRoboticCallBlacklistController extends BaseController
      * */
     @PutMapping
     @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:update')")
-    @Log(title = "修改黑名单信息", businessType = BusinessType.INSERT)
+    @Log(title = "修改黑名单信息", businessType = BusinessType.UPDATE)
     public AjaxResult edit(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
     {
         return toAjax(companyVoiceRoboticCallBlacklistService.updateCompanyVoiceRoboticCallBlacklist(companyVoiceRoboticCallBlacklist));
@@ -106,7 +104,7 @@ public class CompanyVoiceRoboticCallBlacklistController extends BaseController
         return toAjax(companyVoiceRoboticCallBlacklistService.deleteCompanyVoiceRoboticCallBlacklistByIds(blacklistIds));
     }
 
-    @Log(title = "修改黑名单状态", businessType = BusinessType.UPDATE)
+    @Log(title = "修改黑名单失效或启用状态", businessType = BusinessType.UPDATE)
     @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:update')")
     @PutMapping("/changeStatus")
     public AjaxResult changeStatus(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist) {

+ 5 - 6
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java

@@ -10,7 +10,6 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogAddwxService;
-import com.fs.company.service.impl.CompanyUserServiceImpl;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddWxExportVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
@@ -36,13 +35,8 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
 {
     @Autowired
     private ICompanyVoiceRoboticCallLogAddwxService companyVoiceRoboticCallLogAddwxService;
-
     @Autowired
     private TokenService tokenService;
-
-    @Autowired
-    private CompanyUserServiceImpl companyUserService;
-
     /**
      * 查询调用日志_加微信列表
      */
@@ -86,6 +80,10 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
         startPage();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         companyVoiceRoboticCallLogAddwx.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyVoiceRoboticCallLogAddwx.setCompanyUserId(loginUser.getUser().getUserId());
+        }
         List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listAll(companyVoiceRoboticCallLogAddwx);
         TableDataInfo dataTable = getDataTable(list);
         Map<String, Long> countMap = companyVoiceRoboticCallLogAddwxService.countListAll(companyVoiceRoboticCallLogAddwx);
@@ -161,6 +159,7 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
         return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 
+
 //    /**
 //     * 导出调用日志_加微信列表
 //     */

+ 167 - 21
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java

@@ -1,34 +1,32 @@
 package com.fs.company.controller.company;
 
-import java.util.ArrayList;
-import java.util.List;
-
-import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
 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.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.service.ICompanyAiCallDataSyncService;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.service.ICrmCustomerService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * 调用日志_ai打电话Controller
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
@@ -39,6 +37,30 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
+    @Autowired
+    private ICrmCustomerService crmCustomerService;
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ICompanyAiCallDataSyncService companyAiCallDataSyncService;
+
+    /**
+     * 异步同步 AI 外呼数据(补偿 EasyCall 已完成但未回调的记录)
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @Log(title = "同步AI外呼数据", businessType = BusinessType.OTHER)
+    @PostMapping("/syncAiCallData")
+    public AjaxResult syncAiCallData() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+
+        if (!companyAiCallDataSyncService.tryStartSync(companyId)) {
+            return AjaxResult.error("正在同步中,请勿重复操作");
+        }
+        return AjaxResult.success("正在同步中");
+    }
+
     /**
      * 查询调用日志_ai打电话列表
      */
@@ -54,6 +76,7 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
                 companyVoiceRoboticCallLogCallphone.setCallerIds(calleeIds);
                 startPage();
                 List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallLogCallphoneListData(companyVoiceRoboticCallLogCallphone);
+
                 return getDataTable(list);
             } else {
                 return getDataTable(new ArrayList<>());
@@ -74,6 +97,30 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
 
     }
 
+    /**
+     * AI外呼记录明细列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @GetMapping("/detailList")
+    public TableDataInfo detailList(CompanyVoiceRoboticCallLogCallphone param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogCallphoneService.prepareDetailListQuery(param, loginUser.getCompany().getCompanyId());
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = companyVoiceRoboticCallLogCallphoneService.selectDetailList(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * AI外呼记录明细统计
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @GetMapping("/detailSummary")
+    public AjaxResult detailSummary(CompanyVoiceRoboticCallLogCallphone param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogCallphoneService.prepareDetailListQuery(param, loginUser.getCompany().getCompanyId());
+        return AjaxResult.success(companyVoiceRoboticCallLogCallphoneService.selectDetailSummary(param));
+    }
+
 
     /**
      * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
@@ -83,6 +130,14 @@ 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());
+            if (!"00".equals(loginUser.getUser().getUserType())){
+                companyVoiceRoboticCallLogCallphone.setCompanyUserId(loginUser.getUser().getUserId());
+            }
+        }
+
         List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
         return getDataTable(list);
     }
@@ -94,10 +149,72 @@ 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();
+        Long companyUserId=null;
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyUserId=loginUser.getUser().getUserId();
+        }
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogCount(companyId,companyUserId);
         return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 
+    /**
+     * 查询转人工接听记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:handleManualAnswered:query')")
+    @GetMapping("/manualAnsweredList")
+    public TableDataInfo manualAnsweredList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogCallphone.setCompanyId(loginUser.getCompany().getCompanyId());
+        companyVoiceRoboticCallLogCallphone.setManualAnswered(1);
+        if(!loginUser.getUser().getUserType().equals("00")){
+            companyVoiceRoboticCallLogCallphone.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectManualAnsweredList(companyVoiceRoboticCallLogCallphone);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据外呼记录ID查询CRM客户信息(处理数据弹窗用)
+     */
+    @PreAuthorize("@ss.hasPermi('company:handleManualAnswered:handle')")
+    @GetMapping("/getCrmCustomerByLogId")
+    public AjaxResult getCrmCustomerByLogId(@RequestParam("callphoneLogId") Long callphoneLogId)
+    {
+        CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerByCallphoneLogId(callphoneLogId);
+        return AjaxResult.success(crmCustomer);
+    }
+
+    /**
+     * 确认信息(仅标记外呼记录为已处理,不更新客户信息)
+     */
+    @PreAuthorize("@ss.hasPermi('company:handleManualAnswered:handle')")
+    @Log(title = "转人工接听确认信息", businessType = BusinessType.UPDATE)
+    @PostMapping("/confirmManualAnswered")
+    public AjaxResult confirmManualAnswered(@RequestBody CompanyVoiceRoboticCallLogCallphone param)
+    {
+        return toAjax(companyVoiceRoboticCallLogCallphoneService.markHandleFlag(param.getLogId()));
+    }
+
+    /**
+     * 处理数据(标记外呼记录为已处理 + 更新CRM客户信息)
+     */
+    @PreAuthorize("@ss.hasPermi('company:handleManualAnswered:handle')")
+    @Log(title = "转人工接听处理数据", businessType = BusinessType.UPDATE)
+    @PostMapping("/submitManualAnswered")
+    public AjaxResult submitManualAnswered(@RequestBody CrmCustomer param)
+    {
+        // 1. 标记外呼记录为已处理
+        if (param.getLastEffectiveCallphoneLogId() != null) {
+            companyVoiceRoboticCallLogCallphoneService.markHandleFlag(param.getLastEffectiveCallphoneLogId());
+        }
+        // 2. 更新CRM客户信息
+        return toAjax(crmCustomerService.updateCrmCustomer(param));
+    }
+
     /**
      * 导出调用日志_ai打电话列表
      */
@@ -111,6 +228,35 @@ 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)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogCallphoneService.prepareDetailListQuery(companyVoiceRoboticCallLogCallphone, loginUser.getCompany().getCompanyId());
+        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) AjaxResult.success().put("mobile", mobile);
+    }
+
 //    /**
 //     * 导出调用日志_ai打电话列表
 //     */

+ 32 - 19
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java

@@ -1,33 +1,31 @@
 package com.fs.company.controller.company;
 
-import java.util.ArrayList;
-import java.util.List;
-
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogSendmsgService;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
-import org.springframework.security.access.prepost.PreAuthorize;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 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.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
-import com.fs.common.annotation.Log;
-import com.fs.common.core.controller.BaseController;
-import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.enums.BusinessType;
-import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
-import com.fs.company.service.ICompanyVoiceRoboticCallLogSendmsgService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * 调用日志_发送短信Controller
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
@@ -40,6 +38,10 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
 
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
+
+    @Autowired
+    private TokenService tokenService;
+
     /**
      * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
      */
@@ -48,6 +50,11 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
     public TableDataInfo list(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
     {
         startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallLogSendmsg.setCompanyId(loginUser.getCompany().getCompanyId());
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyVoiceRoboticCallLogSendmsg.setCompanyUserId(loginUser.getUser().getUserId());
+        }
         List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgGroupList(companyVoiceRoboticCallLogSendmsg);
         return getDataTable(list);
     }
@@ -85,7 +92,13 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
     @GetMapping("/count")
     public AjaxResult selectCompanyVoiceRoboticCallLogSendMsgCount()
     {
-        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendMsgCount();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId=loginUser.getCompany().getCompanyId();
+        Long companyUserId=null;
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyUserId=loginUser.getUser().getUserId();
+        }
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendMsgCount(companyId,companyUserId);
         return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 

+ 2 - 2
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCalleesController.java

@@ -16,7 +16,7 @@ import java.util.List;
 
 /**
  * 任务外呼电话Controller
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
@@ -89,7 +89,7 @@ public class CompanyVoiceRoboticCalleesController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRoboticCallees:remove')")
     @Log(title = "任务外呼电话", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
         return toAjax(companyVoiceRoboticCalleesService.deleteCompanyVoiceRoboticCalleesByIds(ids));

+ 94 - 48
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -1,7 +1,6 @@
 package com.fs.company.controller.company;
 
 import com.alibaba.fastjson.JSONObject;
-import com.fs.aicall.domain.BaseDomain;
 import com.fs.aicall.domain.TaskInfo;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.result.CIDGroupListResult;
@@ -16,25 +15,27 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.SecurityUtils;
 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.param.AppendCustomersParam;
 import com.fs.company.param.PauseRoboticActiveParam;
 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.service.impl.CompanyUserServiceImpl;
-import com.fs.company.vo.*;
+import com.fs.company.vo.CalleeRoboticCallOutCountVO;
+import com.fs.company.vo.CdrDetailVo;
+import com.fs.company.vo.DocCompanyUserVO;
 import com.fs.crm.domain.CrmCustomerProperty;
 import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
-import com.fs.sop.domain.QwSopTemp;
-import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -88,51 +89,20 @@ public class CompanyVoiceRoboticController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询我的机器人外呼任务列表(所有人只看自己的)
+     */
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:myList')")
     @GetMapping("/myList")
-    public TableDataInfo myList(CompanyVoiceRobotic companyVoiceRobotic){
+    public TableDataInfo myList(CompanyVoiceRobotic companyVoiceRobotic) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
-        companyVoiceRobotic.setCreateUser(loginUser.getUser().getUserId());
+        companyVoiceRobotic.setCompanyUserId(loginUser.getUser().getUserId());
         startPage();
         List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
         fillUserInfo(list);
         return getDataTable(list);
     }
-
-    /**
-     * 填充创建人信息
-     */
-    private void fillUserInfo(List<CompanyVoiceRobotic> list) {
-        if (list == null || list.isEmpty()) {
-            return;
-        }
-
-        Set<Long> userIds = list.stream()
-                .map(CompanyVoiceRobotic::getCreateUser)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
-
-        if (userIds.isEmpty()) {
-            return;
-        }
-
-        Map<Long, DocCompanyUserVO> userMap = companyUserService
-                .selectDocCompanyUserListByUserIds(userIds)
-                .stream()
-                .collect(Collectors.toMap(DocCompanyUserVO::getUserId, Function.identity()));
-
-        list.forEach(item -> {
-            if (item.getCreateUser() != null) {
-                DocCompanyUserVO user = userMap.get(item.getCreateUser());
-                if (user != null) {
-                    item.setCreateByName(user.getNickName());
-                    item.setCreateByDeptName(user.getDeptName());
-                }
-            }
-        });
-    }
-
     /**
      * 查询机器人外呼任务列表
      */
@@ -140,9 +110,26 @@ public class CompanyVoiceRoboticController extends BaseController
     @GetMapping("/listAll")
     public R listAll(CompanyVoiceRobotic companyVoiceRobotic){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (!"00".equals(loginUser.getUser().getUserType())){
+            companyVoiceRobotic.setCompanyUserId(loginUser.getUser().getUserId());
+        }
         companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         return R.ok().put("data", companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic));
     }
+
+    /**
+     * 外呼任务下拉选项(分页 + 名称模糊搜索,按当前登录公司过滤)
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @GetMapping("/selectOptions")
+    public TableDataInfo selectOptions(CompanyVoiceRobotic companyVoiceRobotic) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+        return getDataTable(list);
+    }
+
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:list')")
     @GetMapping("/calleesList")
     public TableDataInfo calleesList(Long id){
@@ -192,7 +179,7 @@ public class CompanyVoiceRoboticController extends BaseController
         return getDataTable(list);
     }
 
-//    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:wxListQw')")
+    //    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:wxListQw')")
     @GetMapping("/wxListQw")
     public TableDataInfo wxListQw(Long id){
         startPage();
@@ -215,6 +202,21 @@ public class CompanyVoiceRoboticController extends BaseController
         return util.exportExcel(list, "robotic");
     }
 
+    /**
+     * 导出我的机器人外呼任务列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:export')")
+    @Log(title = "我的机器人外呼任务", businessType = BusinessType.EXPORT)
+    @GetMapping("/myExport")
+    public AjaxResult myExport(CompanyVoiceRobotic companyVoiceRobotic) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
+        companyVoiceRobotic.setCompanyUserId(loginUser.getUser().getUserId());
+        List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+        ExcelUtil<CompanyVoiceRobotic> util = new ExcelUtil<CompanyVoiceRobotic>(CompanyVoiceRobotic.class);
+        return util.exportExcel(list, "myRobotic");
+    }
+
     /**
      * 获取机器人外呼任务详细信息
      */
@@ -257,7 +259,7 @@ public class CompanyVoiceRoboticController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:remove')")
     @Log(title = "机器人外呼任务", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
         for (Long id : ids) {
@@ -268,7 +270,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 删除机器人外呼任务
      */
-	@GetMapping("/getTypes")
+    @GetMapping("/getTypes")
     public R getTypes(){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
@@ -289,7 +291,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 启动任务
      */
-	@GetMapping("/startRobotic")
+    @GetMapping("/startRobotic")
     public R startRobotic(String taskId){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
@@ -299,7 +301,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 停止任务
      */
-	@GetMapping("/stopRobotic")
+    @GetMapping("/stopRobotic")
     public R stopRobotic(String taskId){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
@@ -309,7 +311,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 员工列表
      */
-	@GetMapping("/companyUserList")
+    @GetMapping("/companyUserList")
     public R qwUserList(){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
@@ -323,7 +325,7 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 外呼回调
      */
-	@PostMapping("/callerResult")
+    @PostMapping("/callerResult")
     public R callerResult(@RequestBody PushIIntentionResult result){
         companyVoiceRoboticService.callerResult(result);
         return R.ok();
@@ -359,6 +361,7 @@ public class CompanyVoiceRoboticController extends BaseController
      * 启动任务
      */
     @GetMapping("/taskRun")
+    @Log(title = "启动机器人外呼任务", businessType = BusinessType.OTHER)
     public R taskRun(Long id){
         companyVoiceRoboticService.taskRun(id);
         return R.ok();
@@ -390,12 +393,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")
@@ -403,10 +407,52 @@ public class CompanyVoiceRoboticController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         return R.ok().put("companyId", loginUser.getCompany().getCompanyId());
     }
+    /**
+     * 填充创建人信息
+     */
+    private void fillUserInfo(List<CompanyVoiceRobotic> list) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
 
+        Set<Long> userIds = list.stream()
+                .map(CompanyVoiceRobotic::getCreateUser)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        if (userIds.isEmpty()) {
+            return;
+        }
+
+        Map<Long, DocCompanyUserVO> userMap = companyUserService
+                .selectDocCompanyUserListByUserIds(userIds)
+                .stream()
+                .collect(Collectors.toMap(DocCompanyUserVO::getUserId, Function.identity()));
+
+        list.forEach(item -> {
+            if (item.getCreateUser() != null) {
+                DocCompanyUserVO user = userMap.get(item.getCreateUser());
+                if (user != null) {
+                    item.setCreateByName(user.getNickName());
+                    item.setCreateByDeptName(user.getDeptName());
+                }
+            }
+        });
+    }
+
+    @Log(title = "外呼任务暂停操作", businessType = BusinessType.UPDATE)
     @PostMapping("/pauseRoboticActive")
     public R pauseRoboticActive(@RequestBody PauseRoboticActiveParam param){
         return companyVoiceRoboticService.pauseRoboticActive(param);
     }
 
+    /**
+     * 追加客户到运行中的普通任务
+     */
+    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:edit')")
+    @Log(title = "追加客户到运行中任务", businessType = BusinessType.INSERT)
+    @PostMapping("/appendCustomers")
+    public R appendCustomers(@RequestBody AppendCustomersParam param){
+        return companyVoiceRoboticService.appendCustomersToRunningTask(param.getTaskId(), param.getCustomerIds());
+    }
 }

+ 2 - 2
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticWxController.java

@@ -16,7 +16,7 @@ import java.util.List;
 
 /**
  * AI外呼任务加微方式Controller
- * 
+ *
  * @author fs
  * @date 2024-12-06
  */
@@ -89,7 +89,7 @@ public class CompanyVoiceRoboticWxController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:VoiceRoboticWx:remove')")
     @Log(title = "AI外呼任务加微方式", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
         return toAjax(companyVoiceRoboticWxService.deleteCompanyVoiceRoboticWxByIds(ids));

+ 7 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java

@@ -239,6 +239,13 @@ public class CompanyWorkflowController extends BaseController {
         return R.ok().put("data",optionVOS);
     }
 
+    @GetMapping("/nodeTypeCodes/{workflowId}")
+    public R getNodeTypeCodes(@PathVariable("workflowId") Long workflowId) {
+        List<String> typeCodes = companyWorkflowService.selectNodeTypeCodesByWorkflowId(workflowId);
+        return R.ok().put("data", typeCodes);
+    }
+
+
     /**
      * 查询某个工作流的版本列表
      */

+ 84 - 5
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java

@@ -21,7 +21,7 @@ import java.util.List;
 
 /**
  * 企微账号Controller
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */
@@ -42,11 +42,23 @@ public class CompanyWxAccountController extends BaseController
     public TableDataInfo list(CompanyWxAccount companyWxEnterpriseAccount)
     {
         startPage();
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxEnterpriseAccount.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxEnterpriseAccount);
         return getDataTable(list);
     }
+
+    /**
+     * 查询我的企微账号列表(仅当前登录人)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myList')")
+    @GetMapping("/myList")
+    public TableDataInfo myList(CompanyWxAccount companyWxAccount)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxAccount.setCompanyUserId(loginUser.getUser().getUserId());
+        startPage();
+        List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxAccount);
+        return getDataTable(list);
+    }
     /**
      * 查询企微账号列表
      */
@@ -74,6 +86,21 @@ public class CompanyWxAccountController extends BaseController
         return util.exportExcel(list, "companyAccount");
     }
 
+    /**
+     * 导出我的企微账号列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myExport')")
+    @Log(title = "我的企微账号", businessType = BusinessType.EXPORT)
+    @GetMapping("/myExport")
+    public AjaxResult myExport(CompanyWxAccount companyWxAccount)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyWxAccount.setCompanyUserId(loginUser.getUser().getUserId());
+        List<CompanyWxAccount> list = companyWxAccountService.selectCompanyWxAccountListCompany(companyWxAccount);
+        ExcelUtil<CompanyWxAccount> util = new ExcelUtil<CompanyWxAccount>(CompanyWxAccount.class);
+        return util.exportExcel(list, "myCompanyAccount");
+    }
+
     /**
      * 获取企微账号详细信息
      */
@@ -91,6 +118,19 @@ public class CompanyWxAccountController extends BaseController
     @Log(title = "企微账号", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody CompanyWxAccount account){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        account.setCompanyId(loginUser.getCompany().getCompanyId());
+        account.setCreateUser(loginUser.getUser().getUserId());
+        return toAjax(companyWxAccountService.insertCompanyWxAccount(account));
+    }
+
+    /**
+     * 新增我的企微账号(仅绑定当前登录人)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myAdd')")
+    @Log(title = "我的企微账号", businessType = BusinessType.INSERT)
+    @PostMapping("/myAdd")
+    public AjaxResult myAdd(@RequestBody CompanyWxAccount account){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         account.setCompanyId(loginUser.getCompany().getCompanyId());
         account.setCompanyUserId(loginUser.getUser().getUserId());
@@ -108,6 +148,25 @@ public class CompanyWxAccountController extends BaseController
         return toAjax(companyWxAccountService.updateCompanyWxAccount(companyWxEnterpriseAccount));
     }
 
+    /**
+     * 修改我的企微账号(仅允许修改当前登录人的数据)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myEdit')")
+    @Log(title = "我的企微账号", businessType = BusinessType.UPDATE)
+    @PutMapping("/myEdit")
+    public AjaxResult myEdit(@RequestBody CompanyWxAccount account)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyUserId = loginUser.getUser().getUserId();
+        CompanyWxAccount existAccount = companyWxAccountService.selectCompanyWxAccountById(account.getId());
+        if (existAccount == null || !companyUserId.equals(existAccount.getCompanyUserId())) {
+            return AjaxResult.error("无权修改该微信账号");
+        }
+        account.setCompanyUserId(companyUserId);
+        account.setCompanyId(loginUser.getCompany().getCompanyId());
+        return toAjax(companyWxAccountService.updateCompanyWxAccount(account));
+    }
+
     /**
      * 删除企微账号
      */
@@ -118,15 +177,35 @@ public class CompanyWxAccountController extends BaseController
     {
         return toAjax(companyWxAccountService.deleteCompanyWxAccountByIds(ids));
     }
+
+    /**
+     * 删除我的企微账号(仅允许删除当前登录人的数据)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyWx:myRemove')")
+    @Log(title = "我的企微账号", businessType = BusinessType.DELETE)
+    @DeleteMapping("/my/{ids}")
+    public AjaxResult myRemove(@PathVariable Long[] ids)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyUserId = loginUser.getUser().getUserId();
+        for (Long id : ids) {
+            CompanyWxAccount account = companyWxAccountService.selectCompanyWxAccountById(id);
+            if (account == null || !companyUserId.equals(account.getCompanyUserId())) {
+                return AjaxResult.error("无权删除该微信账号");
+            }
+        }
+        return toAjax(companyWxAccountService.deleteCompanyWxAccountByIds(ids));
+    }
     /**
      * 删除企微账号
      */
     @PreAuthorize("@ss.hasPermi('company:companyWx:list')")
 	@GetMapping("/companyListAll")
     public R companyListAll(){
-        CompanyUser companyUser = new CompanyUser();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        CompanyUser companyUser = new CompanyUser();
+        companyUser.setCompanyId(companyId);
         return R.ok().put("data", companyWxAccountService.companyListAllCompany(companyUser));
     }
     /**

+ 3 - 4
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxClientController.java

@@ -12,19 +12,18 @@ import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyWxClientService;
 import com.fs.company.vo.AddWxClientVo;
-import com.fs.framework.security.LoginUser;
-import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
-
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 import java.time.LocalDate;
 import java.util.List;
 
 /**
  * 添加个微信账号Controller
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */

+ 4 - 18
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxDialogController.java

@@ -6,12 +6,9 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
-import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyWxDialog;
 import com.fs.company.service.ICompanyWxDialogService;
-import com.fs.framework.security.LoginUser;
-import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -20,7 +17,7 @@ import java.util.List;
 
 /**
  * 添加微信话术Controller
- * 
+ *
  * @author fs
  * @date 2024-12-06
  */
@@ -30,8 +27,6 @@ public class CompanyWxDialogController extends BaseController
 {
     @Autowired
     private ICompanyWxDialogService companyWxDialogService;
-    @Autowired
-    private TokenService tokenService;
 
     /**
      * 查询添加微信话术列表
@@ -40,9 +35,6 @@ public class CompanyWxDialogController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(CompanyWxDialog companyWxDialog)
     {
-
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyWxDialog> list = companyWxDialogService.selectCompanyWxDialogList(companyWxDialog);
         return getDataTable(list);
@@ -51,8 +43,6 @@ public class CompanyWxDialogController extends BaseController
     @PreAuthorize("@ss.hasPermi('company:wxDialog:list')")
     @GetMapping("/listAll")
     public R listAll(CompanyWxDialog companyWxDialog){
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         return R.ok().put("data", companyWxDialogService.selectCompanyWxDialogList(companyWxDialog));
     }
 
@@ -64,8 +54,6 @@ public class CompanyWxDialogController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyWxDialog companyWxDialog)
     {
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyWxDialog> list = companyWxDialogService.selectCompanyWxDialogList(companyWxDialog);
         ExcelUtil<CompanyWxDialog> util = new ExcelUtil<CompanyWxDialog>(CompanyWxDialog.class);
         return util.exportExcel(list, "wxDialog");
@@ -87,10 +75,8 @@ public class CompanyWxDialogController extends BaseController
     @PreAuthorize("@ss.hasPermi('company:wxDialog:add')")
     @Log(title = "添加微信话术", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody CompanyWxDialog companyWxDialog){
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        companyWxDialog.setCompanyId(loginUser.getCompany().getCompanyId());
-        companyWxDialog.setCompanyUserId(loginUser.getUser().getUserId());
+    public AjaxResult add(@RequestBody CompanyWxDialog companyWxDialog)
+    {
         return toAjax(companyWxDialogService.insertCompanyWxDialog(companyWxDialog));
     }
 
@@ -110,7 +96,7 @@ public class CompanyWxDialogController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:wxDialog:remove')")
     @Log(title = "添加微信话术", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
         return toAjax(companyWxDialogService.deleteCompanyWxDialogByIds(ids));

+ 7 - 7
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxUserController.java

@@ -111,15 +111,15 @@ public class CompanyWxUserController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:wxUser:remove')")
     @Log(title = "个微用户", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{userIds}")
+    @DeleteMapping("/{userIds}")
     public AjaxResult remove(@PathVariable Long[] userIds)
     {
         return toAjax(companyWxUserService.deleteCompanyWxUserByUserIds(userIds));
     }
 
     /**
-    *  绑定小程序id
-    */
+     *  绑定小程序id
+     */
     @PutMapping("/bindMiniUserId")
     public R bindUserId(@RequestBody CompanyWxUserBindFsUserParam param)
     {
@@ -127,8 +127,8 @@ public class CompanyWxUserController extends BaseController
     }
 
     /**
-    * 批量添加分组
-    */
+     * 批量添加分组
+     */
 
     @PreAuthorize("@ss.hasPermi('company:wxUser:addGroup')")
     @Log(title = "批量添加分组", businessType = BusinessType.INSERT)
@@ -140,8 +140,8 @@ public class CompanyWxUserController extends BaseController
     }
 
     /**
-    * 批量移除分组
-    */
+     * 批量移除分组
+     */
     @PreAuthorize("@ss.hasPermi('company:wxUser:delGroup')")
     @Log(title = "批量移除分组", businessType = BusinessType.INSERT)
     @PostMapping("/delGroup")

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

@@ -99,7 +99,7 @@ public class CompanyWxUserGroupController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('company:wxUserGroup:remove')")
     @Log(title = "个微 分组", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{groupIds}")
+    @DeleteMapping("/{groupIds}")
     public AjaxResult remove(@PathVariable Long[] groupIds)
     {
         return toAjax(companyWxUserGroupService.deleteCompanyWxUserGroupByGroupIds(groupIds));

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

@@ -239,7 +239,7 @@ public class EasyCallController extends BaseController {
     public R addCommonCallList(@RequestBody EasyCallCommonAddCallListParam param) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getUser().getCompanyId();
-        easyCallService.addCommonCallList(param, companyId);
+        easyCallService.addCommonCallList(param, companyId,null);
         return R.ok("操作成功");
     }
 

+ 170 - 35
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -5,12 +5,15 @@ 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.entity.SysDictData;
 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.Company;
 import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.util.OrderUtils;
@@ -22,7 +25,11 @@ import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerUserService;
 import com.fs.crm.vo.*;
 import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.PermissionService;
 import com.fs.framework.service.TokenService;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.system.service.ISysConfigService;
+import com.fs.system.service.ISysDictTypeService;
 import com.github.pagehelper.PageHelper;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -34,6 +41,7 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -62,6 +70,16 @@ public class CrmCustomerController extends BaseController
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
 
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @Autowired
+    private PermissionService permissionService;
     @ApiOperation("获取线索客户")
     @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
     @GetMapping("/getLineCustomerList")
@@ -70,6 +88,10 @@ public class CrmCustomerController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if(!StringUtils.isEmpty(param.getEncryptedMobile())){
+            param.setMobile(PhoneUtil.encryptPhone(param.getEncryptedMobile()));
+            param.setEncryptedMobile(null);
+        }
         List<CrmLineCustomerListQueryVO> list = crmCustomerService.selectCrmLineCustomerListQuery(param);
         if (list != null) {
             for (CrmLineCustomerListQueryVO vo : list) {
@@ -106,37 +128,65 @@ public class CrmCustomerController extends BaseController
         }
         return getDataTable(list);
     }
-    @PreAuthorize("@ss.hasPermi('crm:customer:list')")
-    @GetMapping("/listAll")
-    public R listAll(CrmCustomerListQueryParam crmCustomer){
-
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
-
-        if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
-            crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
-        }
+//    @PreAuthorize("@ss.hasPermi('crm:customer:list')")
+//    @GetMapping("/listAll")
+//    public R listAll(CrmCustomerListQueryParam crmCustomer){
+//
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+//
+//        if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
+//            crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
+//        }
+//
+//        PageHelper.startPage(1, 1000);
+//        if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
+//            crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
+//        }
+//        List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
+//        if (list != null) {
+//            for (CrmCustomerListVO vo : list) {
+//                if(vo.getMobile()!=null){
+//                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+//                }
+//            }
+//        }
+//        return R.ok().put("rows", list);
+//    }
+@PreAuthorize("@ss.hasPermi('crm:customer:list')")
+@GetMapping("/listAll")
+public TableDataInfo listAll(CrmCustomerListQueryParam crmCustomer){
+    LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+    crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+
+    if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
+        crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
+    }
+    PageHelper.startPage(crmCustomer.getPageNum(), crmCustomer.getPageSize());
+    if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
+        crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
+    }
+    List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
 
-        PageHelper.startPage(1, 1000);
-        if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
-            crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
-        }
-        List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
-        if (list != null) {
-            for (CrmCustomerListVO vo : list) {
-                if(vo.getMobile()!=null){
-                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
-                }
+    if (list != null) {
+        for (CrmCustomerListVO vo : list) {
+            if(vo.getMobile()!=null){
+                vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
             }
         }
-        return R.ok().put("rows", list);
     }
+    return getDataTable(list);
+}
     @PreAuthorize("@ss.hasPermi('crm:customer:list')")
     @GetMapping("/listNoPage")
     public R listNoPage(CrmCustomerListQueryParam crmCustomer){
 
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
+            crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
+        }
         if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
             crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
         }
@@ -144,7 +194,7 @@ public class CrmCustomerController extends BaseController
         if (list != null) {
             for (CrmCustomerListVO vo : list) {
                 if(vo.getMobile()!=null){
-                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                 }
             }
         }
@@ -159,6 +209,7 @@ public class CrmCustomerController extends BaseController
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         param.setCompanyUserId(loginUser.getUser().getUserId());
         param.setCompanyId(loginUser.getCompany().getCompanyId());
+
         if(!StringUtils.isEmpty(param.getCreateTimeRange())){
             param.setCustomerCreateTime(param.getCreateTimeRange().split("--"));
         }
@@ -166,7 +217,7 @@ public class CrmCustomerController extends BaseController
         if (list != null) {
             for (CrmMyCustomerListQueryVO vo : list) {
                 if(vo.getMobile()!=null){
-                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                 }
             }
         }
@@ -188,7 +239,7 @@ public class CrmCustomerController extends BaseController
         if (list != null) {
             for (CrmMyCustomerListQueryVO vo : list) {
                 if(vo.getMobile()!=null){
-                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                 }
             }
             fillMyCustomerListRoboticCallOutCount(list, param.getCompanyId());
@@ -214,7 +265,7 @@ public class CrmCustomerController extends BaseController
             if (list1 != null) {
                 for (CrmLineCustomerListQueryVO vo : list1) {
                     if(vo.getMobile()!=null){
-                        vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                        vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                     }
                     vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
                 }
@@ -226,7 +277,7 @@ public class CrmCustomerController extends BaseController
             if (list != null) {
                 for (CrmCustomerListQueryVO vo : list) {
                     if (vo.getMobile() != null) {
-                        vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                        vo.setMobile(PhoneUtil.maskMobileForList(vo.getMobile()));
                     }
                     vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
 
@@ -311,7 +362,7 @@ public class CrmCustomerController extends BaseController
         CrmCustomer customer=crmCustomerService.selectCrmCustomerById(customerId);
         Boolean isReceive=false;
         if (customer != null){
-            customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+            customer.setMobile(PhoneUtil.maskMobileForList(customer.getMobile()));
             if(customer.getIsReceive()!=null&&customer.getIsReceive()==1&&customer.getReceiveUserId()!=null&&loginUser.getUser().getUserId().equals(customer.getReceiveUserId())){
                 isReceive=true;
             }
@@ -343,6 +394,29 @@ public class CrmCustomerController extends BaseController
 
     }
 
+    //条件分配-统计数量
+    @PreAuthorize("@ss.hasPermi('crm:customer:assignToUser')")
+    @PostMapping("/countByCondition")
+    public R countByCondition(@RequestBody CrmCustomerConditionAssignParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.getQueryParams().put("companyId", loginUser.getCompany().getCompanyId());
+        int count = crmCustomerService.countByCondition(param.getQueryType(), param.getQueryParams());
+        return R.ok().put("count", count);
+    }
+
+    //条件分配-执行分配
+    @PreAuthorize("@ss.hasPermi('crm:customer:assignToUser')")
+    @PostMapping("/assignByCondition")
+    public R assignByCondition(@RequestBody CrmCustomerConditionAssignParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.getQueryParams().put("companyId", loginUser.getCompany().getCompanyId());
+        return crmCustomerService.assignByCondition(loginUser.getUsername(), loginUser.getUser().getUserId(), param);
+    }
+
+
 
     @PreAuthorize("@ss.hasPermi('crm:customer:add')")
     @Log(title = "创建客户", businessType = BusinessType.INSERT)
@@ -484,15 +558,19 @@ public class CrmCustomerController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId(loginUser.getCompany().getCompanyId());
-
-
-
-
         List<CrmCustomerExportVO> list = crmCustomerService.selectCrmCustomerExportListQuery(param);
+        boolean hasQueryPhonePermission = permissionService.hasPermi("crm:customer:queryPhone");
         for(CrmCustomerExportVO customer:list){
             if(StringUtils.isNotEmpty(customer.getMobile())){
-
-                    customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                customer.setMobile(PhoneUtil.maskMobileForList(customer.getMobile()));
+            }
+            if(hasQueryPhonePermission && StringUtils.isNotEmpty(customer.getMobile())){
+                String originalMobile = customer.getMobile().replaceAll("\\*+", "");
+                if(originalMobile.length() >= 11){
+                    customer.setMobilePlain(PhoneUtil.decryptPhone(customer.getMobile()));
+                }else{
+                    customer.setMobilePlain(originalMobile);
+                }
             }
         }
         ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
@@ -504,14 +582,21 @@ public class CrmCustomerController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         List<CrmCustomerExportVO> list = crmCustomerService.selectCrmCustomerExportListByIds(customerIds);
+        boolean hasQueryPhonePermission = permissionService.hasPermi("crm:customer:queryPhone");
         for(CrmCustomerExportVO customer:list){
             if(StringUtils.isNotEmpty(customer.getMobile())){
                 if(loginUser.getUser().getUserType().equals("00")){
+                    if(hasQueryPhonePermission){
+                        if(customer.getMobile().length() > 11){
+                            customer.setMobilePlain(PhoneUtil.decryptPhone(customer.getMobile()));
+                        }else{
+                            customer.setMobilePlain(customer.getMobile());
+                        }
+                    }
                 }
                 else{
-                    customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    customer.setMobile(PhoneUtil.maskMobileForList(customer.getMobile()));
                 }
-
             }
         }
         ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
@@ -544,4 +629,54 @@ public class CrmCustomerController extends BaseController
         return R.ok().put("data",list);
     }
 
+    /**
+     * 查看手机号
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:queryPhone')")
+    @Log(title = "查看手机号", businessType = BusinessType.GRANT)
+    @GetMapping(value = "/queryPhone/{customerId}")
+    public R queryPhone(@PathVariable("customerId") Long customerId)
+    {
+        CrmCustomer customer = crmCustomerService.selectCrmCustomerById(customerId);
+        if(null != customer){
+            String mobile = customer.getMobile();
+            if (mobile != null && mobile.length() > 11) {
+                mobile = PhoneUtil.decryptPhone(mobile);
+            }
+
+            return  R.ok().put("mobile", mobile);
+        }
+
+        return R.error("数据缺失,请联系管理员");
+    }
+
+    @ApiOperation("动态数据字典查询")
+    @GetMapping("/tradeDicts")
+    public AjaxResult getTradeDicts() {
+
+        String[] dictTypes = {"crm_customer_source", "crm_customer_user_status",
+                "crm_customer_type", "crm_customer_tag"};
+        Map<String, List<SysDictData>> result = new HashMap<>();
+
+        for (String dictType : dictTypes) {
+            List<SysDictData> data = null;
+            // 保底逻辑:前缀查不到数据则使用原始字典
+            data = dictTypeService.selectDictDataByType(dictType);
+            result.put(dictType, data);
+        }
+
+        return AjaxResult.success(result);
+    }
+
+    private String getDictRemarkByValue(String dictType, String dictValue) {
+        List<SysDictData> dictDataList = dictTypeService.selectDictDataByType(dictType);
+        if (dictDataList != null) {
+            for (SysDictData data : dictDataList) {
+                if (dictValue.equals(data.getDictValue())) {
+                    return StringUtils.isNotEmpty(data.getRemark()) ? data.getRemark() : "";
+                }
+            }
+        }
+        return "";
+    }
 }

+ 1 - 0
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -120,6 +120,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/msg").anonymous()
                 .antMatchers("/common/getId**").anonymous()
                 .antMatchers("/common/uploadOSS**").anonymous()
+                .antMatchers("/common/proxy/recording").anonymous()
                 .antMatchers("/company/user/common/uploadOSS").anonymous()
                 .antMatchers("/pay/wxPay/payNotify**").anonymous()
                 .antMatchers("/common/uploadWang**").anonymous()

+ 3 - 2
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java

@@ -1,12 +1,13 @@
 package com.fs.aiSipCall.domain;
 
-import java.util.Date;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.util.Date;
+
 /**
  * sip用户信息对象 ai_sip_call_user
  *

+ 3 - 3
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java

@@ -1,12 +1,12 @@
 package com.fs.aiSipCall.service;
 
-import java.util.List;
-import java.util.Map;
-
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.aiSipCall.domain.AiSipCallUser;
 import com.fs.common.core.domain.AjaxResult;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * sip用户信息Service接口
  *

+ 2 - 1
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallVoiceTtsAliyunService.java

@@ -1,9 +1,10 @@
 package com.fs.aiSipCall.service;
 
-import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.aiSipCall.domain.AiSipCallVoiceTtsAliyun;
 
+import java.util.List;
+
 /**
  * aiSIP外呼阿里云音色Service接口
  *

+ 6 - 12
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java

@@ -15,7 +15,6 @@ import com.fs.aiSipCall.param.ApiCallRecordByUuidQueryParams;
 import com.fs.aiSipCall.param.ApiCallRecordQueryParams;
 import com.fs.aiSipCall.service.IAiSipCallOutboundCdrService;
 import com.fs.aiSipCall.utils.DateUtils;
-import com.fs.aiSipCall.vo.ApiCallRecordQueryVo;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.company.domain.CompanyAiWorkflowExec;
@@ -25,11 +24,8 @@ import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
 import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
 import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
 import com.fs.company.mapper.EasyCallMapper;
-import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.CompanyWorkflowEngine;
-import com.fs.company.service.impl.call.node.AiCallTaskNode;
 import com.fs.company.vo.CidConfigVO;
-import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.company.vo.easycall.EasyCallOutBoundVO;
 import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
@@ -40,8 +36,6 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.net.URLEncoder;
-import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -51,7 +45,7 @@ import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORK
 
 /**
  * aiSIP手动外呼通话记录Service业务层处理
- * 
+ *
  * @author fs
  * @date 2026-03-19
  */
@@ -91,7 +85,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
     }
     /**
      * 查询aiSIP手动外呼通话记录列表
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return aiSIP手动外呼通话记录
      */
@@ -128,7 +122,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 新增aiSIP手动外呼通话记录
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return 结果
      */
@@ -140,7 +134,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 修改aiSIP手动外呼通话记录
-     * 
+     *
      * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
      * @return 结果
      */
@@ -152,7 +146,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 批量删除aiSIP手动外呼通话记录
-     * 
+     *
      * @param ids 需要删除的aiSIP手动外呼通话记录主键
      * @return 结果
      */
@@ -164,7 +158,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
 
     /**
      * 删除aiSIP手动外呼通话记录信息
-     * 
+     *
      * @param id aiSIP手动外呼通话记录主键
      * @return 结果
      */

+ 13 - 12
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java

@@ -1,19 +1,20 @@
 package com.fs.aiSipCall.service.impl;
-import java.util.List;
-import java.util.Map;
 
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallUser;
+import com.fs.aiSipCall.mapper.AiSipCallUserMapper;
+import com.fs.aiSipCall.service.IAiSipCallUserService;
 import com.fs.common.core.domain.AjaxResult;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.company.mapper.CompanyUserMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import com.fs.aiSipCall.mapper.AiSipCallUserMapper;
-import com.fs.aiSipCall.domain.AiSipCallUser;
-import com.fs.aiSipCall.service.IAiSipCallUserService;
+
+import java.util.List;
+import java.util.Map;
 
 /**
  * sip用户信息Service业务层处理
@@ -30,7 +31,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 查询sip用户信息
-     * 
+     *
      * @param userId sip用户信息主键
      * @return sip用户信息
      */
@@ -42,7 +43,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 查询sip用户信息列表
-     * 
+     *
      * @param aiSipCallUser sip用户信息
      * @return sip用户信息
      */
@@ -54,7 +55,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 新增sip用户信息
-     * 
+     *
      * @param aiSipCallUser sip用户信息
      * @return 结果
      */
@@ -102,7 +103,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 修改sip用户信息
-     * 
+     *
      * @param aiSipCallUser sip用户信息
      * @return 结果
      */
@@ -126,7 +127,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 批量删除sip用户信息
-     * 
+     *
      * @param userIds 需要删除的sip用户信息主键
      * @return 结果
      */
@@ -138,7 +139,7 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
 
     /**
      * 删除sip用户信息信息
-     * 
+     *
      * @param userId sip用户信息主键
      * @return 结果
      */

+ 1 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java

@@ -104,6 +104,7 @@ public class CompanyVoiceRobotic {
     private String runTaskFlow;
     // 任务状态0待执行1执行中2执行中断3执行完成
     private Integer taskStatus;
+    private String pauseSource;
     private Integer addWxTime;
     /** 创建人 */
     @Excel(name = "创建时间")

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

@@ -3,8 +3,8 @@ package com.fs.company.domain;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 /**

+ 8 - 6
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java

@@ -1,18 +1,17 @@
 package com.fs.company.domain;
 
-import java.util.Date;
-import java.util.List;
-
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
-import com.fs.common.constant.Constants;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.util.Date;
+import java.util.List;
+
 /**
  * 调用日志_加微信对象 company_voice_robotic_call_log_addwx
  *
@@ -57,6 +56,9 @@ public class CompanyVoiceRoboticCallLogAddwx extends BaseEntity{
     @Excel(name = "公司id")
     private Long companyId;
 
+    @TableField(exist = false)
+    private Long companyUserId;
+
     /** 个微账号id */
     @Excel(name = "个微账号id")
     private Long wxAccountId;

+ 70 - 11
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java

@@ -1,19 +1,18 @@
 package com.fs.company.domain;
 
-import java.math.BigDecimal;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
-
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
 /**
  * 调用日志_ai打电话对象 company_voice_robotic_call_log_callphone
  *
@@ -106,9 +105,38 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @Excel(name = "外呼类型")
     private Integer callType;
 
-    /** 回调重试次数 */
-    @Excel(name = "回调重试次数")
-    private Integer retryCount;
+    /** 未拨通类型 */
+    @Excel(name = "未拨通类型")
+    private String hangupType;
+
+    /** 是否警告(0否 1是)用于敏感词 */
+    @Excel(name = "是否警告(0否 1是)用于敏感词")
+    private Integer isWarning;
+
+    /**
+     * 违规次数
+     * **/
+    private Integer violationNum;
+    /**
+     * 转人工接听:1:是,0或无:否
+     */
+    private Integer manualAnswered;
+    /**
+     * 是否被处理:0:未被处理,1:已处理
+     */
+    private Integer handleFlag;
+    /**
+     * 接听分机号码
+     */
+    private String answeredExtNum;
+    /**
+     * 人工接听时间
+     */
+    private Long manualAnsweredTime;
+    /**
+     * 人工接听时长
+     */
+    private Long manualAnsweredTimeLen;
 
     @TableField(exist = false)
     private String companyName;
@@ -122,6 +150,23 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @TableField(exist = false)
     private List<Long> callerIds;
 
+    @TableField(exist = false)
+    private Integer isConnected;
+
+    @TableField(exist = false)
+    private Long minCallTime;
+
+    @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;
 
@@ -137,8 +182,22 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @TableField(exist = false)
     private Integer runningCount;
 
+    /** 详情筛选-运行时间起(yyyy-MM-dd) */
+    @TableField(exist = false)
+    private String beginRunTime;
+
+    /** 详情筛选-运行时间止(yyyy-MM-dd) */
     @TableField(exist = false)
-    private String successRate;
+    private String endRunTime;
+
+    /** 详情筛选-客户类型为「无」 */
+    @TableField(exist = false)
+    private Boolean intentionEmpty;
+
+    /**
+     * 重试次数
+     */
+    private Integer retryCount;
 
     public static CompanyVoiceRoboticCallLogCallphone initCallLog( String runParam, Long keyId, Long taskId,Long companyId) {
         CompanyVoiceRoboticCallLogCallphone log = new CompanyVoiceRoboticCallLogCallphone();

+ 6 - 6
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java

@@ -1,18 +1,18 @@
 package com.fs.company.domain;
 
-import java.math.BigDecimal;
-import java.util.Date;
-import java.util.List;
-
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
-import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
 /**
  * 调用日志_发送短信对象 company_voice_robotic_call_log_sendmsg
  *

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

@@ -7,7 +7,7 @@ import lombok.Data;
 
 /**
  * AI外呼任务加微方式对象 company_voice_robotic_wx
- * 
+ *
  * @author fs
  * @date 2024-12-06
  */

+ 47 - 0
fs-service/src/main/java/com/fs/company/domain/OutboundLineLimitLog.java

@@ -0,0 +1,47 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("outbound_line_limit_log")
+public class OutboundLineLimitLog {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    private Long gatewayId;
+
+    /** 时间粒度(分钟): 30/60/120/360/1440 */
+    private Integer timeWindow;
+
+    /** 时间窗口起始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime windowStart;
+
+    /** 该窗口最大允许呼出次数 */
+    private Integer maxCalls;
+
+    /** 当前窗口已呼叫次数 */
+    private Integer currentCount;
+
+    /** 是否触发限制: 0否 1是 */
+    private Integer isLimited;
+
+    /** 触发时的调用参数JSON */
+    private String callParam;
+
+    /** 下一个可用时间点 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime nextAvailableTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+}

+ 26 - 0
fs-service/src/main/java/com/fs/company/dto/OutboundLimitResultVO.java

@@ -0,0 +1,26 @@
+package com.fs.company.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class OutboundLimitResultVO {
+    private boolean passed;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date nextAvailableTime;
+
+    public static OutboundLimitResultVO pass() {
+        OutboundLimitResultVO r = new OutboundLimitResultVO();
+        r.passed = true;
+        return r;
+    }
+
+    public static OutboundLimitResultVO limited(Date nextAvailableTime) {
+        OutboundLimitResultVO r = new OutboundLimitResultVO();
+        r.passed = false;
+        r.nextAvailableTime = nextAvailableTime;
+        return r;
+    }
+}

+ 36 - 8
fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java

@@ -11,14 +11,14 @@ import java.util.List;
 
 /**
  * AI外呼工作流执行Mapper接口
- * 
+ *
  * @author fs
  * @date 2026-01-28
  */
 public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflowExec>{
     /**
      * 查询AI外呼工作流执行
-     * 
+     *
      * @param id AI外呼工作流执行主键
      * @return AI外呼工作流执行
      */
@@ -26,7 +26,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 查询AI外呼工作流执行列表
-     * 
+     *
      * @param companyAiWorkflowExec AI外呼工作流执行
      * @return AI外呼工作流执行集合
      */
@@ -34,7 +34,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 新增AI外呼工作流执行
-     * 
+     *
      * @param companyAiWorkflowExec AI外呼工作流执行
      * @return 结果
      */
@@ -42,7 +42,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 修改AI外呼工作流执行
-     * 
+     *
      * @param companyAiWorkflowExec AI外呼工作流执行
      * @return 结果
      */
@@ -52,7 +52,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 删除AI外呼工作流执行
-     * 
+     *
      * @param id AI外呼工作流执行主键
      * @return 结果
      */
@@ -60,7 +60,7 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
 
     /**
      * 批量删除AI外呼工作流执行
-     * 
+     *
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */
@@ -106,14 +106,42 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
      * **/
     int insertBatchInfo(@Param("list") List<CompanyAiWorkflowExec> list);
 //    List<WorkflowExecRecordVo> selectExecRecordsByRoboticId(@Param("roboticId") Long roboticId);
+
     List<WorkflowExecRecordVo> selectExecRecordsByRoboticId(
             @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);
 
     Long selectWxClientIdByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
+
+    /**
+     * 原子认领待执行任务:仅当当前状态等于 expectStatus 时,才将状态更新为 targetStatus。
+     * 用于防止定时扫描在上一轮任务尚未完成时重复调度同一条记录。
+     * @param id 执行记录主键
+     * @param expectStatus 期望的原状态(如 READY=9)
+     * @param targetStatus 认领后的目标状态(如 RUNNING=3)
+     * @return 影响行数,1 表示认领成功,0 表示已被其它线程/上一轮认领
+     */
+    int claimExecForRun(@Param("id") Long id,
+                        @Param("expectStatus") Integer expectStatus,
+                        @Param("targetStatus") Integer targetStatus);
+
+    /**
+     * 回扫超时认领任务:将处于 runningStatus 且 last_update_time 早于 timeoutTime 的记录重置为 failStatus,
+     * 防止进程重启 / 线程池拒绝导致任务永久卡在认领态。
+     * @param groupNo cid 分组号
+     * @param runningStatus 认领态(如 RUNNING=3)
+     * @param failStatus 重置目标态(如 FAILURE=2)
+     * @param timeoutTime 超时时间界限,last_update_time 早于该时间的视为卡死
+     * @return 影响行数
+     */
+    int resetTimeoutRunningExec(@Param("groupNo") Integer groupNo,
+                                @Param("runningStatus") Integer runningStatus,
+                                @Param("failStatus") Integer failStatus,
+                                @Param("timeoutTime") java.time.LocalDateTime timeoutTime);
 }

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

@@ -26,8 +26,17 @@ public interface CompanyVoiceCloneRefMapper {
     List<CcTtsAliyun> selectCcTtsAliyunList();
 
 
+    /**
+     * 分页查询公司音色关联列表
+     * @param ref 查询条件(companyId必填,companyUserId可选用于子账号过滤)
+     * @return 音色关联列表
+     */
+    List<CompanyVoiceCloneRef> selectCompanyVoiceCloneRefList(CompanyVoiceCloneRef ref);
 
+    int deleteById(@Param("id") Long id);
 
+    int updateStatusById(CompanyVoiceCloneRef ref);
 
 
-}
+
+}

+ 17 - 5
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -4,10 +4,7 @@ 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 com.fs.company.vo.*;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -83,11 +80,18 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
 
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
-    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount(@Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
 
 
     List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
+    List<CompanyVoiceRoboticCallLogCallPhoneVO> selectDetailList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    CompanyVoiceRoboticCallLogDetailSummary selectDetailSummary(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    List<CompanyVoiceRoboticCallLogCallPhoneDecryptQueryVO> listDecryptPhoneExport(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+
     /**
      * 根据业务ID查询公司ID
      * @param businessId 业务ID (bes.id)
@@ -95,6 +99,9 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      */
     Long selectCompanyIdByBusinessId(@Param("businessId") Long businessId);
 
+    List<CompanyVoiceRoboticCallLogCallphone> selectManualAnsweredList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+
     /**
      * 按客户统计 AI 外呼呼出次数(callphone.caller_id = callees.id,callees.user_id = 客户 customer_id)
      */
@@ -117,4 +124,9 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      * 更新重试次数和状态
      */
     int updateRetryCountAndStatus(CompanyVoiceRoboticCallLogCallphone record);
+
+    /**
+     * 查询执行中任务下、外呼记录仍为执行中的 callbackUuid 列表
+     */
+    List<String> selectRunningCallbackUuidsByCompanyId(@Param("companyId") Long companyId);
 }

+ 9 - 8
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogSendmsgMapper.java

@@ -5,17 +5,18 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 调用日志_发送短信Mapper接口
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
 public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<CompanyVoiceRoboticCallLogSendmsg>{
     /**
      * 查询调用日志_发送短信
-     * 
+     *
      * @param logId 调用日志_发送短信主键
      * @return 调用日志_发送短信
      */
@@ -23,7 +24,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 查询调用日志_发送短信列表
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 调用日志_发送短信集合
      */
@@ -33,7 +34,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 新增调用日志_发送短信
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 结果
      */
@@ -41,7 +42,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 修改调用日志_发送短信
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 结果
      */
@@ -49,7 +50,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 删除调用日志_发送短信
-     * 
+     *
      * @param logId 调用日志_发送短信主键
      * @return 结果
      */
@@ -57,7 +58,7 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     /**
      * 批量删除调用日志_发送短信
-     * 
+     *
      * @param logIds 需要删除的数据主键集合
      * @return 结果
      */
@@ -65,5 +66,5 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
 
     List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
 
-    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount();
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount(@Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
 }

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

@@ -8,6 +8,8 @@ import com.fs.company.vo.easycall.EasyCallOutBoundVO;
 import org.apache.ibatis.annotations.Param;
 import org.springframework.stereotype.Repository;
 
+import java.util.List;
+
 /**
  * @author MixLiu
  * @date 2026/3/7 10:43
@@ -29,4 +31,9 @@ public interface EasyCallMapper {
     @DataSource(DataSourceType.EASYCALL)
     EasyCallCallPhoneVO getCallPhoneInfoByCallBackUuid(@Param("callBackUuid") String callBackUuid);
 
+    /**
+     * 根据 callbackUuid 批量查询 EasyCall 已外呼完成的话单 uuid
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<String> selectCompletedUuidsByCallbackUuids(@Param("callbackUuids") List<String> callbackUuids);
 }

+ 18 - 0
fs-service/src/main/java/com/fs/company/mapper/OutboundLineLimitLogMapper.java

@@ -0,0 +1,18 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.OutboundLineLimitLog;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+
+public interface OutboundLineLimitLogMapper extends BaseMapper<OutboundLineLimitLog> {
+
+    int asyncInsert(OutboundLineLimitLog log);
+
+    int countByWindow(@Param("companyId") Long companyId,
+                      @Param("gatewayId") Long gatewayId,
+                      @Param("timeWindow") Integer timeWindow,
+                      @Param("windowStart") LocalDateTime windowStart,
+                      @Param("windowEnd") LocalDateTime windowEnd);
+}

+ 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;
+
+}

+ 5 - 3
fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java

@@ -70,9 +70,11 @@ public interface CompanyWorkflowEngine {
      * @param workFlowId
      */
     Long createSipTask(Long roboticId,Long workFlowId);
+
     /**
-     * 添加微信成功
-     * @param object
+     * 更新exec记录的variables字段(用于暂停时同步延时上下文到DB)
+     * @param workflowInstanceId
+     * @param variables 最新的variables map
      */
-    void addWxSuccess(JSONObject object);
+    void updateExecVariables(String workflowInstanceId, Map<String, Object> variables);
 }

+ 15 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyAiCallDataSyncService.java

@@ -0,0 +1,15 @@
+package com.fs.company.service;
+
+/**
+ * AI外呼数据同步服务
+ */
+public interface ICompanyAiCallDataSyncService {
+
+    /**
+     * 尝试启动异步同步任务(同公司防重复)
+     *
+     * @param companyId 公司ID
+     * @return true-已启动;false-已有任务在运行
+     */
+    boolean tryStartSync(Long companyId);
+}

+ 9 - 8
fs-service/src/main/java/com/fs/company/service/ICompanyInboundBindService.java

@@ -1,19 +1,20 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyInboundBind;
 
+import java.util.List;
+
 /**
  * 呼入线路模型绑定关系Service接口
- * 
+ *
  * @author fs
  * @date 2026-04-27
  */
 public interface ICompanyInboundBindService extends IService<CompanyInboundBind>{
     /**
      * 查询呼入线路模型绑定关系
-     * 
+     *
      * @param id 呼入线路模型绑定关系主键
      * @return 呼入线路模型绑定关系
      */
@@ -21,7 +22,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 查询呼入线路模型绑定关系列表
-     * 
+     *
      * @param companyInboundBind 呼入线路模型绑定关系
      * @return 呼入线路模型绑定关系集合
      */
@@ -29,7 +30,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 新增呼入线路模型绑定关系
-     * 
+     *
      * @param companyInboundBind 呼入线路模型绑定关系
      * @return 结果
      */
@@ -37,7 +38,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 修改呼入线路模型绑定关系
-     * 
+     *
      * @param companyInboundBind 呼入线路模型绑定关系
      * @return 结果
      */
@@ -45,7 +46,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 批量删除呼入线路模型绑定关系
-     * 
+     *
      * @param ids 需要删除的呼入线路模型绑定关系主键集合
      * @return 结果
      */
@@ -53,7 +54,7 @@ public interface ICompanyInboundBindService extends IService<CompanyInboundBind>
 
     /**
      * 删除呼入线路模型绑定关系信息
-     * 
+     *
      * @param id 呼入线路模型绑定关系主键
      * @return 结果
      */

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

@@ -75,4 +75,6 @@ public interface ICompanyInboundCallManageService {
      * @return 通话记录列表
      */
     List<EasyCallInboundCdrVO> selectInboundCdrList(EasyCallInboundCdrVO vo);
+
+    Boolean checkCalleeInboundLlm(String callee);
 }

+ 10 - 9
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceApiService.java

@@ -1,19 +1,20 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.fs.company.domain.CompanyVoiceApi;
 
+import java.util.List;
+
 /**
  * 呼叫接口Service接口
- * 
+ *
  * @author fs
  * @date 2021-10-04
  */
-public interface ICompanyVoiceApiService 
+public interface ICompanyVoiceApiService
 {
     /**
      * 查询呼叫接口
-     * 
+     *
      * @param apiId 呼叫接口ID
      * @return 呼叫接口
      */
@@ -21,7 +22,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 查询呼叫接口列表
-     * 
+     *
      * @param companyVoiceApi 呼叫接口
      * @return 呼叫接口集合
      */
@@ -29,7 +30,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 新增呼叫接口
-     * 
+     *
      * @param companyVoiceApi 呼叫接口
      * @return 结果
      */
@@ -37,7 +38,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 修改呼叫接口
-     * 
+     *
      * @param companyVoiceApi 呼叫接口
      * @return 结果
      */
@@ -45,7 +46,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 批量删除呼叫接口
-     * 
+     *
      * @param apiIds 需要删除的呼叫接口ID
      * @return 结果
      */
@@ -53,7 +54,7 @@ public interface ICompanyVoiceApiService
 
     /**
      * 删除呼叫接口信息
-     * 
+     *
      * @param apiId 呼叫接口ID
      * @return 结果
      */

+ 10 - 9
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceBlacklistService.java

@@ -1,19 +1,20 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.fs.company.domain.CompanyVoiceBlacklist;
 
+import java.util.List;
+
 /**
  * 黑名单Service接口
- * 
+ *
  * @author fs
  * @date 2023-02-23
  */
-public interface ICompanyVoiceBlacklistService 
+public interface ICompanyVoiceBlacklistService
 {
     /**
      * 查询黑名单
-     * 
+     *
      * @param blacklistId 黑名单ID
      * @return 黑名单
      */
@@ -21,7 +22,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 查询黑名单列表
-     * 
+     *
      * @param companyVoiceBlacklist 黑名单
      * @return 黑名单集合
      */
@@ -29,7 +30,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 新增黑名单
-     * 
+     *
      * @param companyVoiceBlacklist 黑名单
      * @return 结果
      */
@@ -37,7 +38,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 修改黑名单
-     * 
+     *
      * @param companyVoiceBlacklist 黑名单
      * @return 结果
      */
@@ -45,7 +46,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 批量删除黑名单
-     * 
+     *
      * @param blacklistIds 需要删除的黑名单ID
      * @return 结果
      */
@@ -53,7 +54,7 @@ public interface ICompanyVoiceBlacklistService
 
     /**
      * 删除黑名单信息
-     * 
+     *
      * @param blacklistId 黑名单ID
      * @return 结果
      */

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

@@ -1,11 +1,12 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.fs.company.domain.CompanyVoiceCaller;
 import com.fs.company.param.CompanyVoiceCallerImportParam;
 import com.fs.company.param.CompanyVoiceCallerParam;
 import com.fs.company.vo.CompanyVoiceCallerListVO;
 
+import java.util.List;
+
 /**
  * 企业用户坐席Service接口
  *

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

@@ -1,8 +1,6 @@
 package com.fs.company.service;
 
-import com.fs.common.core.domain.AjaxResult;
 import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
-import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 

+ 27 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneService.java

@@ -1,8 +1,11 @@
 package com.fs.company.service;
 
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyVoiceCloneRef;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.List;
+
 /**
  * 豆包声音克隆 Service 接口
  *
@@ -33,4 +36,28 @@ public interface ICompanyVoiceCloneService {
      * @return 操作结果(含 base64 音频数据)
      */
     AjaxResult doubaoTtsTest(String speakerId, Integer language, String text);
+
+    /**
+     * 分页查询音色关联列表
+     *
+     * @param ref 查询条件(companyId、companyUserId、voiceName、status)
+     * @return 音色关联列表
+     */
+    List<CompanyVoiceCloneRef> selectRefList(CompanyVoiceCloneRef ref);
+
+    /**
+     * 根据ID删除音色关联
+     *
+     * @param id 主键ID
+     * @return 删除行数
+     */
+    int deleteRefById(Long id);
+
+    /**
+     * 修改音色关联启用/禁用状态
+     *
+     * @param ref 包含 id 和 status
+     * @return 更新行数
+     */
+    int changeStatus(CompanyVoiceCloneRef ref);
 }

+ 10 - 9
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceConfigService.java

@@ -1,20 +1,21 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.fs.company.domain.CompanyVoiceConfig;
 import com.fs.company.vo.CompanyVoiceConfigListVO;
 
+import java.util.List;
+
 /**
  * 呼叫频率配置Service接口
- * 
+ *
  * @author fs
  * @date 2021-11-13
  */
-public interface ICompanyVoiceConfigService 
+public interface ICompanyVoiceConfigService
 {
     /**
      * 查询呼叫频率配置
-     * 
+     *
      * @param configId 呼叫频率配置ID
      * @return 呼叫频率配置
      */
@@ -22,7 +23,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 查询呼叫频率配置列表
-     * 
+     *
      * @param companyVoiceConfig 呼叫频率配置
      * @return 呼叫频率配置集合
      */
@@ -30,7 +31,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 新增呼叫频率配置
-     * 
+     *
      * @param companyVoiceConfig 呼叫频率配置
      * @return 结果
      */
@@ -38,7 +39,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 修改呼叫频率配置
-     * 
+     *
      * @param companyVoiceConfig 呼叫频率配置
      * @return 结果
      */
@@ -46,7 +47,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 批量删除呼叫频率配置
-     * 
+     *
      * @param configIds 需要删除的呼叫频率配置ID
      * @return 结果
      */
@@ -54,7 +55,7 @@ public interface ICompanyVoiceConfigService
 
     /**
      * 删除呼叫频率配置信息
-     * 
+     *
      * @param configId 呼叫频率配置ID
      * @return 结果
      */

+ 3 - 3
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceLogsService.java

@@ -1,8 +1,5 @@
 package com.fs.company.service;
 
-import java.util.List;
-import java.util.Map;
-
 import com.alibaba.fastjson.JSONObject;
 import com.fs.company.domain.CompanyVoiceLogs;
 import com.fs.company.param.CompanyStatisticsParam;
@@ -13,6 +10,9 @@ import com.fs.company.vo.CompanyVoiceLogsListVO;
 import com.fs.company.vo.CompanyVoiceLogsStatisticsVO;
 import com.fs.company.vo.CompanyVoiceLogsVO;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * 通话记录Service接口
  *

+ 3 - 5
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogAddwxService.java

@@ -1,15 +1,14 @@
 package com.fs.company.service;
 
-import java.util.List;
-import java.util.Map;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
-import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * 调用日志_加微信Service接口
  * 
@@ -90,5 +89,4 @@ public interface ICompanyVoiceRoboticCallLogAddwxService extends IService<Compan
     List<CompanyVoiceRoboticCallLogAddwxVO> listByRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
 
 
-
 }

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

@@ -1,13 +1,10 @@
 package com.fs.company.service;
 
-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;
+import com.fs.company.vo.*;
+
+import java.util.List;
 
 /**
  * 调用日志_ai打电话Service接口
@@ -84,10 +81,34 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
 
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
-    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount(Long companyId,Long companyUserId);
 
     List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
     List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId);
 
+
+    void prepareDetailListQuery(CompanyVoiceRoboticCallLogCallphone query, Long companyId);
+
+    List<CompanyVoiceRoboticCallLogCallPhoneVO> selectDetailList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    CompanyVoiceRoboticCallLogDetailSummary selectDetailSummary(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    List<CompanyVoiceRoboticCallLogCallPhoneDecryptExportVO> listDecryptPhoneExport(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    List<CompanyVoiceRoboticCallLogCallphone> selectManualAnsweredList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    /**
+     * 标记外呼记录为已处理(handleFlag=1)
+     *
+     * @param logId 外呼记录主键
+     * @return 影响行数
+     */
+    int markHandleFlag(Long logId);
+
+    /**
+     * 根据外呼记录ID获取解密后的手机号
+     */
+    String getDecryptPhoneByLogId(Long logId);
+
 }

+ 10 - 9
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogSendmsgService.java

@@ -1,21 +1,22 @@
 package com.fs.company.service;
 
-import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 
+import java.util.List;
+
 /**
  * 调用日志_发送短信Service接口
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
 public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<CompanyVoiceRoboticCallLogSendmsg>{
     /**
      * 查询调用日志_发送短信
-     * 
+     *
      * @param logId 调用日志_发送短信主键
      * @return 调用日志_发送短信
      */
@@ -23,7 +24,7 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
 
     /**
      * 查询调用日志_发送短信列表
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 调用日志_发送短信集合
      */
@@ -33,7 +34,7 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
 
     /**
      * 新增调用日志_发送短信
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 结果
      */
@@ -41,7 +42,7 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
 
     /**
      * 修改调用日志_发送短信
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 结果
      */
@@ -49,7 +50,7 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
 
     /**
      * 批量删除调用日志_发送短信
-     * 
+     *
      * @param logIds 需要删除的调用日志_发送短信主键集合
      * @return 结果
      */
@@ -57,7 +58,7 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
 
     /**
      * 删除调用日志_发送短信信息
-     * 
+     *
      * @param logId 调用日志_发送短信主键
      * @return 结果
      */
@@ -65,5 +66,5 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
 
     List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
 
-    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount();
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount(Long companyId,Long companyUserId);
 }

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

@@ -7,7 +7,10 @@ import com.fs.common.core.domain.R;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.param.PauseRoboticActiveParam;
-import com.fs.company.vo.*;
+import com.fs.company.vo.AddWxClientVo;
+import com.fs.company.vo.AiCallConfigVO;
+import com.fs.company.vo.CdrDetailVo;
+import com.fs.company.vo.CompanyVoiceRoboticQwUserListVo;
 
 import java.util.List;
 import java.util.Map;
@@ -15,14 +18,14 @@ import java.util.Set;
 
 /**
  * 机器人外呼任务Service接口
- * 
+ *
  * @author fs
  * @date 2024-12-04
  */
 public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRobotic> {
     /**
      * 查询机器人外呼任务
-     * 
+     *
      * @param id 机器人外呼任务ID
      * @return 机器人外呼任务
      */
@@ -30,7 +33,7 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     /**
      * 查询机器人外呼任务列表
-     * 
+     *
      * @param companyVoiceRobotic 机器人外呼任务
      * @return 机器人外呼任务集合
      */
@@ -39,7 +42,7 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     /**
      * 新增机器人外呼任务
-     * 
+     *
      * @param companyVoiceRobotic 机器人外呼任务
      * @return 结果
      */
@@ -52,7 +55,7 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     /**
      * 修改机器人外呼任务
-     * 
+     *
      * @param companyVoiceRobotic 机器人外呼任务
      * @return 结果
      */
@@ -60,7 +63,7 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     /**
      * 批量删除机器人外呼任务
-     * 
+     *
      * @param ids 需要删除的机器人外呼任务ID
      * @return 结果
      */
@@ -68,7 +71,7 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     /**
      * 删除机器人外呼任务信息
-     * 
+     *
      * @param id 机器人外呼任务ID
      * @return 结果
      */
@@ -76,7 +79,7 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     /**
      * 更新删除标志(逻辑删除)
-     * 
+     *
      * @param id 机器人外呼任务ID
      * @param delFlag 删除标志 0正常 1删除
      * @return 结果
@@ -103,7 +106,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);
 
@@ -116,4 +119,22 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
     void addNewExec4Task(Long taskId,Long crmCustomerId,String traceId);
 
     R pauseRoboticActive(PauseRoboticActiveParam param);
+
+    /**
+     * 判断任务是否处于暂停状态
+     * 优先从Redis读取,Redis无数据则查DB并回填
+     *
+     * @param taskId 任务ID
+     * @return true=暂停中 false=非暂停
+     */
+    boolean isTaskPaused(Long taskId);
+
+    /**
+     * 追加客户到运行中的普通任务
+     *
+     * @param taskId      任务ID
+     * @param customerIds 要追加的CRM客户ID列表
+     * @return 追加结果(成功数、重复客户信息)
+     */
+    R appendCustomersToRunningTask(Long taskId, List<Long> customerIds);
 }

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

@@ -107,4 +107,6 @@ public interface ICompanyWorkflowService {
     List<CompanyWorkflowNodeVoiceVo> getMyWorkflowNodes(Long companyUserId);
 
     List<OptionVO> optionList(Long companyId);
+
+    List<String> selectNodeTypeCodesByWorkflowId(Long workflowId);
 }

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

@@ -9,7 +9,7 @@ import java.util.List;
 
 /**
  * 企微账号Service接口
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */
@@ -17,7 +17,7 @@ public interface ICompanyWxAccountService extends IService<CompanyWxAccount>
 {
     /**
      * 查询企微账号
-     * 
+     *
      * @param id 企微账号ID
      * @return 企微账号
      */
@@ -25,7 +25,7 @@ public interface ICompanyWxAccountService extends IService<CompanyWxAccount>
 
     /**
      * 查询企微账号列表
-     * 
+     *
      * @param companyWxAccount 企微账号
      * @return 企微账号集合
      */
@@ -34,7 +34,7 @@ public interface ICompanyWxAccountService extends IService<CompanyWxAccount>
 
     /**
      * 新增企微账号
-     * 
+     *
      * @param companyWxAccount 企微账号
      * @return 结果
      */
@@ -42,7 +42,7 @@ public interface ICompanyWxAccountService extends IService<CompanyWxAccount>
 
     /**
      * 修改企微账号
-     * 
+     *
      * @param companyWxAccount 企微账号
      * @return 结果
      */
@@ -50,7 +50,7 @@ public interface ICompanyWxAccountService extends IService<CompanyWxAccount>
 
     /**
      * 批量删除企微账号
-     * 
+     *
      * @param ids 需要删除的企微账号ID
      * @return 结果
      */
@@ -58,7 +58,7 @@ public interface ICompanyWxAccountService extends IService<CompanyWxAccount>
 
     /**
      * 删除企微账号信息
-     * 
+     *
      * @param id 企微账号ID
      * @return 结果
      */

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

@@ -1,20 +1,20 @@
 package com.fs.company.service;
 
-import java.util.List;
-
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyWxChat;
 
+import java.util.List;
+
 /**
  * 个微聊天Service接口
- * 
+ *
  * @author fs
  * @date 2025-01-15
  */
 public interface ICompanyWxChatService extends IService<CompanyWxChat> {
     /**
      * 查询个微聊天
-     * 
+     *
      * @param id 个微聊天主键
      * @return 个微聊天
      */
@@ -22,7 +22,7 @@ public interface ICompanyWxChatService extends IService<CompanyWxChat> {
 
     /**
      * 查询个微聊天列表
-     * 
+     *
      * @param companyWxChat 个微聊天
      * @return 个微聊天集合
      */
@@ -30,7 +30,7 @@ public interface ICompanyWxChatService extends IService<CompanyWxChat> {
 
     /**
      * 新增个微聊天
-     * 
+     *
      * @param companyWxChat 个微聊天
      * @return 结果
      */
@@ -38,7 +38,7 @@ public interface ICompanyWxChatService extends IService<CompanyWxChat> {
 
     /**
      * 修改个微聊天
-     * 
+     *
      * @param companyWxChat 个微聊天
      * @return 结果
      */
@@ -46,7 +46,7 @@ public interface ICompanyWxChatService extends IService<CompanyWxChat> {
 
     /**
      * 批量删除个微聊天
-     * 
+     *
      * @param ids 需要删除的个微聊天主键集合
      * @return 结果
      */
@@ -54,7 +54,7 @@ public interface ICompanyWxChatService extends IService<CompanyWxChat> {
 
     /**
      * 删除个微聊天信息
-     * 
+     *
      * @param id 个微聊天主键
      * @return 结果
      */

+ 7 - 9
fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java

@@ -9,14 +9,14 @@ import java.util.List;
 
 /**
  * 添加个微信账号Service接口
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */
 public interface ICompanyWxClientService extends IService<CompanyWxClient> {
     /**
      * 查询添加个微信账号
-     * 
+     *
      * @param id 添加个微信账号ID
      * @return 添加个微信账号
      */
@@ -24,7 +24,7 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
 
     /**
      * 查询添加个微信账号列表
-     * 
+     *
      * @param companyWxClient 添加个微信账号
      * @return 添加个微信账号集合
      */
@@ -33,7 +33,7 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
 
     /**
      * 新增添加个微信账号
-     * 
+     *
      * @param companyWxClient 添加个微信账号
      * @return 结果
      */
@@ -41,7 +41,7 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
 
     /**
      * 修改添加个微信账号
-     * 
+     *
      * @param companyWxClient 添加个微信账号
      * @return 结果
      */
@@ -49,7 +49,7 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
 
     /**
      * 批量删除添加个微信账号
-     * 
+     *
      * @param ids 需要删除的添加个微信账号ID
      * @return 结果
      */
@@ -57,7 +57,7 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
 
     /**
      * 删除添加个微信账号信息
-     * 
+     *
      * @param id 添加个微信账号ID
      * @return 结果
      */
@@ -73,8 +73,6 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
 
     List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(List<Long> accountIdList,Integer cidGroupId);
 
-    List<CompanyWxClient4WorkFlowVO> getAddWxList4WorkflowNew(List<Long> accountIdList, Integer cidGroupId);
-
     List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList,Integer isWeCom,Integer cidGroupNo);
 
     List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(List<Long> accountIdList,Integer cidGroupNo);

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

@@ -7,14 +7,14 @@ import java.util.List;
 
 /**
  * 添加微信话术Service接口
- * 
+ *
  * @author fs
  * @date 2024-12-06
  */
 public interface ICompanyWxDialogService extends IService<CompanyWxDialog> {
     /**
      * 查询添加微信话术
-     * 
+     *
      * @param id 添加微信话术ID
      * @return 添加微信话术
      */
@@ -22,7 +22,7 @@ public interface ICompanyWxDialogService extends IService<CompanyWxDialog> {
 
     /**
      * 查询添加微信话术列表
-     * 
+     *
      * @param companyWxDialog 添加微信话术
      * @return 添加微信话术集合
      */
@@ -30,7 +30,7 @@ public interface ICompanyWxDialogService extends IService<CompanyWxDialog> {
 
     /**
      * 新增添加微信话术
-     * 
+     *
      * @param companyWxDialog 添加微信话术
      * @return 结果
      */
@@ -38,7 +38,7 @@ public interface ICompanyWxDialogService extends IService<CompanyWxDialog> {
 
     /**
      * 修改添加微信话术
-     * 
+     *
      * @param companyWxDialog 添加微信话术
      * @return 结果
      */
@@ -46,7 +46,7 @@ public interface ICompanyWxDialogService extends IService<CompanyWxDialog> {
 
     /**
      * 批量删除添加微信话术
-     * 
+     *
      * @param ids 需要删除的添加微信话术ID
      * @return 结果
      */
@@ -54,7 +54,7 @@ public interface ICompanyWxDialogService extends IService<CompanyWxDialog> {
 
     /**
      * 删除添加微信话术信息
-     * 
+     *
      * @param id 添加微信话术ID
      * @return 结果
      */

+ 320 - 4
fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java

@@ -1,11 +1,19 @@
 package com.fs.company.service.easycall;
 
+import cn.hutool.core.date.DateUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyConfig;
+import com.fs.company.domain.OutboundLineLimitLog;
+import com.fs.company.dto.OutboundLimitResultVO;
+import com.fs.company.mapper.CompanyConfigMapper;
 import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.mapper.OutboundLineLimitLogMapper;
 import com.fs.company.vo.CidConfigVO;
 import com.fs.company.vo.easycall.*;
 import cn.hutool.http.HttpRequest;
@@ -16,10 +24,16 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 /**
  * EasyCallCenter365 外呼服务实现类
@@ -36,6 +50,13 @@ public class EasyCallServiceImpl implements IEasyCallService {
 
     @Autowired
     private ISysConfigService configService;
+
+    @Autowired
+    private CompanyConfigMapper companyConfigMapper;
+
+    @Autowired
+    private OutboundLineLimitLogMapper outboundLineLimitLogMapper;
+
     /**
      * EasyCallCenter365 服务器基础地址,从配置文件 easycall.base-url 读取
      */
@@ -88,8 +109,20 @@ public class EasyCallServiceImpl implements IEasyCallService {
      */
     private static final String API_COMMON_ADD_LIST = "/aicall/api/common/addCallList";
 
+    /** 外呼线路执行限制 Redis key 前缀 */
+    private static final String OUTBOUND_LIMIT_REDIS_PREFIX = "outbound:limit:";
+
     @Autowired
     CompanyMapper companyMapper;
+
+    @Autowired
+    private RedisCacheT<String> redisCacheT;
+
+    @Autowired
+    private RedisCache redisCache;
+
+
+
     // =================== 基础数据查询接口 ===================
 
     /**
@@ -248,10 +281,291 @@ public class EasyCallServiceImpl implements IEasyCallService {
      * bizJson 字段可传入需要传递给机器人的业务数据(如客户姓名、订单号等)
      */
     @Override
-    public void addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId) {
+    public boolean addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId,Long gatewayId) {
+//        String url = buildUrl(API_COMMON_ADD_LIST);
+//        JSONObject result = doPost(url, JSON.toJSONString(param));
+//        checkSuccess(result);
+        // 1. 检查外呼线路限制
+        OutboundLimitResultVO limitResult = checkOutboundLineLimit(companyId, gatewayId);
+        if (!limitResult.isPassed()) {
+            log.warn("addCommonCallList: 外呼线路执行限制触发 - companyId: {}, gatewayId: {}, 下次可用时间: {}",
+                    companyId, gatewayId, DateUtil.format(limitResult.getNextAvailableTime(), "yyyy-MM-dd HH:mm:ss"));
+            // 将未执行的呼叫参数存入 Redis,供定时任务重试
+            saveRetryTask(companyId, gatewayId, param, limitResult.getNextAvailableTime());
+            return false;
+        }
+        // 2. 调用外呼接口追加名单
         String url = buildUrl(API_COMMON_ADD_LIST);
-        JSONObject result = doPost(url, JSON.toJSONString(param));
-        checkSuccess(result);
+        try {
+            JSONObject result = doPost(url, JSON.toJSONString(param));
+            boolean success = checkSuccess(result);
+            if (success) {
+                // 3. 执行成功后记录OutboundLineLimitLog日志
+                saveExecutionLog(companyId, gatewayId, param);
+            }
+            return success;
+        } catch (Exception e) {
+            log.error("addCommonCallList: 外呼接口调用异常 - companyId: {}, gatewayId: {}", companyId, gatewayId, e);
+            return false;
+        }
+    }
+    /**
+     * 外呼执行成功后,记录 OutboundLineLimitLog 日志
+     * <p>
+     * 仅做数据库写入,不维护Redis计数缓存(计数以数据库为准)
+     *
+     * @param companyId 公司ID
+     * @param gatewayId 外呼线路(网关)ID
+     * @param callParam 调用参数(记录到日志便于追溯)
+     */
+    private void saveExecutionLog(Long companyId, Long gatewayId, EasyCallCommonAddCallListParam callParam) {
+        try {
+            if (gatewayId == null) {
+                return;
+            }
+
+            // 查询配置,获取匹配的规则
+            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(companyId, "cId.config");
+            if (companyConfig == null || StringUtils.isEmpty(companyConfig.getConfigValue())) {
+                return;
+            }
+
+            JSONObject configJson = JSONObject.parseObject(companyConfig.getConfigValue());
+            Boolean enabled = configJson.getBoolean("outboundLineLimitEnabled");
+            if (enabled == null || !enabled) {
+                return;
+            }
+
+            JSONArray rules = configJson.getJSONArray("outboundLineRules");
+            if (rules == null || rules.isEmpty()) {
+                return;
+            }
+
+            LocalDateTime now = LocalDateTime.now();
+            LocalDateTime todayMidnight = now.toLocalDate().atStartOfDay();
+
+            IntStream.range(0, rules.size())
+                    .mapToObj(rules::getJSONObject)
+                    .filter(rule -> {
+                        Integer tw = rule.getInteger("timeWindow");
+                        Integer mc = rule.getInteger("maxCalls");
+                        return tw != null && tw > 0 && mc != null && mc > 0;
+                    })
+                    .filter(rule -> {
+                        JSONArray lineIdsArr = rule.getJSONArray("lineIds");
+                        if (lineIdsArr == null || lineIdsArr.isEmpty()) {
+                            return true;
+                        }
+                        return IntStream.range(0, lineIdsArr.size())
+                                .anyMatch(j -> gatewayId.equals(lineIdsArr.getLong(j)));
+                    })
+                    .forEach(rule -> {
+                        Integer timeWindow = rule.getInteger("timeWindow");
+                        Integer maxCalls = rule.getInteger("maxCalls");
+
+                        // 计算当前固定窗口(以零点对齐)
+                        long minutesSinceMidnight = Duration.between(todayMidnight, now).toMinutes();
+                        long windowIndex = minutesSinceMidnight / timeWindow;
+                        LocalDateTime windowStart = todayMidnight.plusMinutes(windowIndex * timeWindow);
+                        LocalDateTime windowEnd = windowStart.plusMinutes(timeWindow);
+
+                        // 查询当前窗口内的实际数量
+                        int currentCount = outboundLineLimitLogMapper.countByWindow(companyId, gatewayId, timeWindow, windowStart, windowEnd);
+
+                        // 记录成功执行日志(is_limited=0)
+                        saveLimitLog(companyId, gatewayId, timeWindow, windowStart, maxCalls,
+                                currentCount + 1, callParam, null, 0);
+                    });
+        } catch (Exception e) {
+            log.error("saveExecutionLog 异常 - companyId: {}, gatewayId: {}", companyId, gatewayId, e);
+        }
+    }
+
+    /**
+     * 保存外呼重试任务到 Redis(Sorted Set 结构)
+     * <p>
+     * ZSET key: outbound:limit:retry:zset
+     * score: nextAvailableTime 的毫秒时间戳(定时任务用 ZRANGEBYSCORE 查询已到期的任务)
+     * member: JSON 字符串,包含 companyId, gatewayId, param, nextAvailableTime, createTime
+     *
+     * @param companyId        公司ID
+     * @param gatewayId        网关ID
+     * @param param            外呼调用参数(定时任务重试时需要)
+     * @param nextAvailableTime 下一个可用时间点
+     */
+    private void saveRetryTask(Long companyId, Long gatewayId, EasyCallCommonAddCallListParam param, Date nextAvailableTime) {
+        try {
+            JSONObject retryData = new JSONObject();
+            retryData.put("companyId", companyId);
+            retryData.put("gatewayId", gatewayId);
+            retryData.put("param", param);
+            retryData.put("nextAvailableTime", nextAvailableTime.getTime());
+            retryData.put("createTime", System.currentTimeMillis());
+
+            // score = nextAvailableTime 时间戳,定时任务通过 ZRANGEBYSCORE 0 当前时间 查询到期任务
+            String zsetKey = OUTBOUND_LIMIT_REDIS_PREFIX + "retry:zset";
+            double score = nextAvailableTime.getTime();
+            redisCache.zSetAdd(zsetKey, retryData.toJSONString(), score);
+
+            log.info("saveRetryTask: 已保存重试任务到ZSET - companyId: {}, gatewayId: {}, nextAvailableTime: {}",
+                    companyId, gatewayId, DateUtil.format(nextAvailableTime, "yyyy-MM-dd HH:mm:ss"));
+        } catch (Exception e) {
+            log.error("saveRetryTask 异常 - companyId: {}, gatewayId: {}", companyId, gatewayId, e);
+        }
+    }
+
+
+    /**
+     * 检查外呼线路是否达到限制
+     * <p>
+     * 流程:
+     * 1. 根据 gatewayId 线路ID找到公司配置
+     * 2. 如果配置了外呼限制(outboundLineLimitEnabled=true),筛选匹配当前线路的规则
+     * 3. 直接从 OutboundLineLimitLog 数据库查询时间窗口内的实际外呼次数(不依赖Redis缓存计数)
+     * 4. 如果达到限制则返回 limited 状态,上一级方法中断执行
+     * 5. 如果未达到限制则返回 pass 状态,上一级方法继续执行
+     *
+     * @param companyId 公司ID
+     * @param gatewayId 外呼线路(网关)ID
+     * @return 限制检查结果
+     */
+    private OutboundLimitResultVO checkOutboundLineLimit(Long companyId, Long gatewayId) {
+        try {
+            // gatewayId为空时不做限制
+            if (gatewayId == null) {
+                return OutboundLimitResultVO.pass();
+            }
+
+            // 1. 查询公司配置
+            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(companyId, "cId.config");
+            if (companyConfig == null || StringUtils.isEmpty(companyConfig.getConfigValue())) {
+                return OutboundLimitResultVO.pass();
+            }
+
+            // 2. 检查是否启用外呼限制
+            JSONObject configJson = JSONObject.parseObject(companyConfig.getConfigValue());
+            Boolean enabled = configJson.getBoolean("outboundLineLimitEnabled");
+            if (enabled == null || !enabled) {
+                return OutboundLimitResultVO.pass();
+            }
+
+            JSONArray rules = configJson.getJSONArray("outboundLineRules");
+            if (rules == null || rules.isEmpty()) {
+                return OutboundLimitResultVO.pass();
+            }
+
+            // 3. 预筛选:找出匹配当前 gatewayId 的规则(纯内存操作,无I/O)
+            List<JSONObject> matchedRules = IntStream.range(0, rules.size())
+                    .mapToObj(rules::getJSONObject)
+                    .filter(rule -> {
+                        Integer tw = rule.getInteger("timeWindow");
+                        Integer mc = rule.getInteger("maxCalls");
+                        return tw != null && tw > 0 && mc != null && mc > 0;
+                    })
+                    .filter(rule -> {
+                        JSONArray lineIdsArr = rule.getJSONArray("lineIds");
+                        if (lineIdsArr == null || lineIdsArr.isEmpty()) {
+                            return true; // 全局规则,匹配所有线路
+                        }
+                        return IntStream.range(0, lineIdsArr.size())
+                                .anyMatch(j -> gatewayId.equals(lineIdsArr.getLong(j)));
+                    })
+                    .collect(Collectors.toList());
+
+            if (matchedRules.isEmpty()) {
+                return OutboundLimitResultVO.pass();
+            }
+
+            // 4. 对匹配的规则逐一检查限制(数量通常很少,1-3条)
+            LocalDateTime now = LocalDateTime.now();
+            LocalDateTime todayMidnight = now.toLocalDate().atStartOfDay();
+            Date earliestNextTime = null;
+
+            for (JSONObject rule : matchedRules) {
+                Integer timeWindow = rule.getInteger("timeWindow");
+                Integer maxCalls = rule.getInteger("maxCalls");
+
+                // 计算当前固定窗口(以零点对齐)
+                // 例:timeWindow=30 → 00:00~00:29, 00:30~00:59, ...
+                // 例:timeWindow=60 → 00:00~00:59, 01:00~01:59, ...
+                // 例:timeWindow=1440 → 00:00~23:59(整天)
+                long minutesSinceMidnight = Duration.between(todayMidnight, now).toMinutes();
+                long windowIndex = minutesSinceMidnight / timeWindow;
+                LocalDateTime windowStart = todayMidnight.plusMinutes(windowIndex * timeWindow);
+                LocalDateTime windowEnd = windowStart.plusMinutes(timeWindow);
+
+                // 快速拒绝:检查 Redis blocked 标记
+                String blockedRedisKey = OUTBOUND_LIMIT_REDIS_PREFIX + companyId + ":" + gatewayId + ":blocked:" + timeWindow + ":" + windowIndex;
+                String cachedBlock = redisCacheT.getCacheObject(blockedRedisKey);
+                if (cachedBlock != null) {
+                    Date nextAvailable = JSON.parseObject(cachedBlock, Date.class);
+                    if (nextAvailable != null) {
+                        if (earliestNextTime == null || nextAvailable.before(earliestNextTime)) {
+                            earliestNextTime = nextAvailable;
+                        }
+                        continue;
+                    }
+                }
+
+                // 查数据库:当前窗口内的实际外呼数量
+                int currentCount = outboundLineLimitLogMapper.countByWindow(companyId, gatewayId, timeWindow, windowStart, windowEnd);
+
+                // 判断是否达到限制
+                if (currentCount >= maxCalls) {
+                    // nextAvailableTime = 下一个窗口的起始时间(定时任务扫描用)
+                    Date nextAvailable = Date.from(windowEnd.atZone(ZoneId.systemDefault()).toInstant());
+
+                    // 设置 Redis blocked 标记,过期时间到当前窗口结束
+                    long remainingMinutes = Duration.between(now, windowEnd).toMinutes();
+                    remainingMinutes = Math.max(1, remainingMinutes);
+                    redisCacheT.setCacheObject(blockedRedisKey, JSON.toJSONString(nextAvailable),
+                            remainingMinutes, TimeUnit.MINUTES);
+
+                    if (earliestNextTime == null || nextAvailable.before(earliestNextTime)) {
+                        earliestNextTime = nextAvailable;
+                    }
+
+                    // 记录限制触发日志
+                    LocalDateTime nextAvailableTime = windowEnd;
+                    saveLimitLog(companyId, gatewayId, timeWindow, windowStart, maxCalls,
+                            currentCount, null, nextAvailableTime, 1);
+                }
+            }
+
+            // 5. 返回结果
+            if (earliestNextTime != null) {
+                return OutboundLimitResultVO.limited(earliestNextTime);
+            }
+            return OutboundLimitResultVO.pass();
+
+        } catch (Exception e) {
+            log.error("checkOutboundLineLimit 异常,默认放行 - companyId: {}, gatewayId: {}", companyId, gatewayId, e);
+            return OutboundLimitResultVO.pass();
+        }
+    }
+
+    /**
+     * 保存外呼限制日志到数据库
+     */
+    private void saveLimitLog(Long companyId, Long gatewayId, Integer timeWindow, LocalDateTime windowStart,
+                              Integer maxCalls, int currentCount, Object callParam,
+                              LocalDateTime nextAvailableTime, int isLimited) {
+        try {
+            OutboundLineLimitLog logEntity = new OutboundLineLimitLog();
+            logEntity.setCompanyId(companyId);
+            logEntity.setGatewayId(gatewayId);
+            logEntity.setTimeWindow(timeWindow);
+            logEntity.setWindowStart(windowStart);
+            logEntity.setMaxCalls(maxCalls);
+            logEntity.setCurrentCount(currentCount);
+            logEntity.setIsLimited(isLimited);
+            logEntity.setCallParam(callParam != null ? JSON.toJSONString(callParam) : null);
+            logEntity.setNextAvailableTime(nextAvailableTime);
+            logEntity.setCreateTime(LocalDateTime.now());
+            outboundLineLimitLogMapper.asyncInsert(logEntity);
+        } catch (Exception e) {
+            log.error("saveLimitLog 异常 - companyId: {}, gatewayId: {}, isLimited: {}", companyId, gatewayId, isLimited, e);
+        }
     }
 
     // =================== 录音相关接口 ===================
@@ -358,8 +672,10 @@ public class EasyCallServiceImpl implements IEasyCallService {
      * 启动、停止、追加名单等操作类接口无 data 返回,
      * 成功与否已由 parseAndCheck 中的 code 判断保证,此处无需额外处理
      */
-    private void checkSuccess(JSONObject result) {
+    private boolean checkSuccess(JSONObject result) {
         // parseAndCheck 中 code != 0 时已抛异常,走到这里即代表操作成功
+        Integer code = result.getInteger("code");
+        return code != null && code == 0;
     }
 
     /**

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

@@ -92,13 +92,21 @@ public interface IEasyCallService {
      */
     void addAiCallList(EasyCallAddCallListParam param, Long companyId);
 
+//    /**
+//     * 通用追加名单(支持AI外呼和通知提醒任务)
+//     *
+//     * @param param     追加参数
+//     * @param companyId 公司id
+//     */
+//    void addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId);
     /**
      * 通用追加名单(支持AI外呼和通知提醒任务)
      *
      * @param param     追加参数
      * @param companyId 公司id
+     * @param gatewayId
      */
-    void addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId);
+    boolean addCommonCallList(EasyCallCommonAddCallListParam param, Long companyId, Long gatewayId);
 
     /**
      * 获取录音文件访问URL

+ 84 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyAiCallDataSyncServiceImpl.java

@@ -0,0 +1,84 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.core.redis.RedisCache;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
+import com.fs.company.mapper.EasyCallMapper;
+import com.fs.company.service.ICompanyAiCallDataSyncService;
+import com.fs.company.service.ICompanyVoiceRoboticService;
+import com.fs.company.vo.CdrDetailVo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * AI外呼数据同步:补偿 EasyCall 已外呼完成但未回调入库的记录
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CompanyAiCallDataSyncServiceImpl implements ICompanyAiCallDataSyncService {
+
+    private static final String SYNC_LOCK_PREFIX = "ai_call_sync:company:";
+    private static final int BATCH_SIZE = 1000;
+    private static final long LOCK_TIMEOUT_HOURS = 2L;
+
+    private final RedisCache redisCache;
+    private final CompanyVoiceRoboticCallLogCallphoneMapper callLogCallphoneMapper;
+    private final EasyCallMapper easyCallMapper;
+    private final ICompanyVoiceRoboticService companyVoiceRoboticService;
+
+
+    @Override
+    public boolean tryStartSync(Long companyId) {
+        String lockKey = buildLockKey(companyId);
+        if (!redisCache.setIfAbsent(lockKey, "1", LOCK_TIMEOUT_HOURS, TimeUnit.HOURS)) {
+            return false;
+        }
+        doSyncAiCallDataAsync(companyId, lockKey);
+        return true;
+    }
+
+    @Async("callLogExcutor")
+    public void doSyncAiCallDataAsync(Long companyId, String lockKey) {
+        try {
+            List<String> callbackUuids = callLogCallphoneMapper.selectRunningCallbackUuidsByCompanyId(companyId);
+            if (callbackUuids == null || callbackUuids.isEmpty()) {
+                log.info("syncAiCallData: 无待补偿外呼记录, companyId={}", companyId);
+                return;
+            }
+            log.info("syncAiCallData: 开始同步, companyId={}, 待检查callbackUuid数={}", companyId, callbackUuids.size());
+            int triggered = 0;
+            for (int i = 0; i < callbackUuids.size(); i += BATCH_SIZE) {
+                List<String> batch = callbackUuids.subList(i, Math.min(i + BATCH_SIZE, callbackUuids.size()));
+                List<String> completedUuids = easyCallMapper.selectCompletedUuidsByCallbackUuids(batch);
+                if (completedUuids == null || completedUuids.isEmpty()) {
+                    continue;
+                }
+                for (String uuid : completedUuids) {
+                    try {
+                        CdrDetailVo cdrDetailVo = new CdrDetailVo();
+                        cdrDetailVo.setUuid(uuid);
+                        cdrDetailVo.setCdrType("outbound");
+                        companyVoiceRoboticService.callerResult4EasyCall(cdrDetailVo);
+                        triggered++;
+                    } catch (Exception e) {
+                        log.error("syncAiCallData: 触发回调失败, uuid={}, companyId={}", uuid, companyId, e);
+                    }
+                }
+            }
+            log.info("syncAiCallData: 同步完成, companyId={}, 触发回调数={}", companyId, triggered);
+        } catch (Exception e) {
+            log.error("syncAiCallData: 同步异常, companyId={}", companyId, e);
+        } finally {
+            redisCache.deleteObject(lockKey);
+        }
+    }
+
+    private String buildLockKey(Long companyId) {
+        return SYNC_LOCK_PREFIX + companyId;
+    }
+}

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

@@ -1,22 +1,22 @@
 package com.fs.company.service.impl;
 
-import java.util.List;
-import java.util.Random;
-
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.company.domain.CompanyAiWorkflowServer;
 import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.CompanyAiWorkflowServerMapper;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.company.param.BindCidServerParam;
+import com.fs.company.service.ICompanyAiWorkflowServerService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyAiWorkflowServerMapper;
-import com.fs.company.domain.CompanyAiWorkflowServer;
-import com.fs.company.service.ICompanyAiWorkflowServerService;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.List;
+import java.util.Random;
+
 /**
  * cid服务Service业务层处理
  *

+ 16 - 4
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneServiceImpl.java

@@ -18,10 +18,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -359,6 +356,21 @@ public class CompanyVoiceCloneServiceImpl implements ICompanyVoiceCloneService {
         }
     }
 
+    @Override
+    public List<CompanyVoiceCloneRef> selectRefList(CompanyVoiceCloneRef ref) {
+        return companyVoiceCloneRefMapper.selectCompanyVoiceCloneRefList(ref);
+    }
+
+    @Override
+    public int deleteRefById(Long id) {
+        return companyVoiceCloneRefMapper.deleteById(id);
+    }
+
+    @Override
+    public int changeStatus(CompanyVoiceCloneRef ref) {
+        return companyVoiceCloneRefMapper.updateStatusById(ref);
+    }
+
     /**
      * 构建 TTS 请求体
      */

+ 0 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallBlacklistServiceImpl.java

@@ -1,7 +1,6 @@
 package com.fs.company.service.impl;
 
 import com.fs.common.utils.DateUtils;
-import com.fs.common.utils.ServletUtils;
 import com.fs.company.domain.CompanyVoiceRoboticCallBlacklist;
 import com.fs.company.mapper.CompanyVoiceRoboticCallBlacklistMapper;
 import com.fs.company.param.CompanyVoiceRoboticCallBlacklistCheckParam;

+ 8 - 14
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogAddwxServiceImpl.java

@@ -1,25 +1,22 @@
 package com.fs.company.service.impl;
 
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.fs.company.domain.CompanyVoiceRoboticCallLog;
-import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogAddwxMapper;
 import com.fs.company.mapper.CompanyWxClientMapper;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogAddwxService;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
-import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogAddwxMapper;
-import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
-import com.fs.company.service.ICompanyVoiceRoboticCallLogAddwxService;
+
+import java.util.List;
+import java.util.Map;
 
 /**
  * 调用日志_加微信Service业务层处理
@@ -29,9 +26,7 @@ import com.fs.company.service.ICompanyVoiceRoboticCallLogAddwxService;
  */
 @Service
 @Slf4j
-public
-
-class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<CompanyVoiceRoboticCallLogAddwxMapper, CompanyVoiceRoboticCallLogAddwx> implements ICompanyVoiceRoboticCallLogAddwxService {
+public class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<CompanyVoiceRoboticCallLogAddwxMapper, CompanyVoiceRoboticCallLogAddwx> implements ICompanyVoiceRoboticCallLogAddwxService {
 
 
     @Autowired
@@ -164,7 +159,6 @@ class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<CompanyVoic
     public Map<String, Long> countListAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx){
         return baseMapper.countListAll(companyVoiceRoboticCallLogAddwx);
     }
-
     @Override
     public List<CompanyVoiceRoboticCallLogAddwx> selectCompanyVoiceRoboticAddwxLogGroupList(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx) {
         return baseMapper.selectCompanyVoiceRoboticCallLogAddwxGroupList(companyVoiceRoboticCallLogAddwx);

+ 457 - 226
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -1,43 +1,50 @@
 package com.fs.company.service.impl;
 
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.*;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executor;
-
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.fs.aicall.domain.TaskInfo;
-import com.fs.aicall.domain.apiresult.Notify;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
-import com.fs.aicall.domain.param.getDialogMapDomain;
 import com.fs.aicall.service.AiCallService;
 import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.service.CompanyWorkflowEngine;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.vo.*;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
-import com.fs.crm.service.ICrmCustomerAnalyzeService;
+import com.fs.core.config.TenantConfigContext;
 import com.fs.crm.service.ICrmCustomerPropertyService;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.sensitive.DTO.AgentSensitiveWordDetectResultDTO;
+import com.fs.sensitive.component.AgentSensitiveWordDetector;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.impl.SysDictTypeServiceImpl;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Service;
-import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+import static com.fs.company.service.impl.call.node.AbstractWorkflowNode.companyVoiceRoboticMapper;
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
 
 /**
@@ -71,13 +78,20 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Autowired
     QwUserMapper qwUserMapper;
     @Autowired
-    private ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
-    @Autowired
     @Qualifier("cidWorkFlowExecutor")
     private Executor cidWorkFlowExecutor;
 
     @Autowired
     SysDictTypeServiceImpl sysDictTypeService;
+    @Autowired
+    @Lazy
+    ICompanyVoiceRoboticService companyVoiceRoboticService;
+    @Autowired
+    CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
+
+    @Autowired
+    private AgentSensitiveWordDetector agentSensitiveWordDetector;
+
     /**
      * 查询调用日志_ai打电话
      *
@@ -175,200 +189,309 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
 
     @Async("callLogExcutor")
     public void asyncHandleCalleeCallBackResult(PushIIntentionResult result, CompanyVoiceRoboticCallees callees) {
-        try {
-//            String runTaskFlow = callees.getRunTaskFlow();
-//            if(StringUtils.isBlank(runTaskFlow)){
-//                runTaskFlow = Constants.CELL_PHONE;
-//            }else{
-//                if (!runTaskFlow.contains(Constants.CELL_PHONE)) {
-//                    runTaskFlow = runTaskFlow + "," + Constants.CELL_PHONE;
-//                }
+//        try {
+////            String runTaskFlow = callees.getRunTaskFlow();
+////            if(StringUtils.isBlank(runTaskFlow)){
+////                runTaskFlow = Constants.CELL_PHONE;
+////            }else{
+////                if (!runTaskFlow.contains(Constants.CELL_PHONE)) {
+////                    runTaskFlow = runTaskFlow + "," + Constants.CELL_PHONE;
+////                }
+////            }
+////            if(!runTaskFlow.equals(callees.getRunTaskFlow())){
+////                CompanyVoiceRoboticCallees updateCallees = new CompanyVoiceRoboticCallees();
+////                updateCallees.setId(callees.getId());
+////                updateCallees.setRunTaskFlow(runTaskFlow);
+////                companyVoiceRoboticCalleesMapper.updateById(updateCallees);
+////            }
+//            String json = configService.selectConfigByKey("cid.config");
+//            if (StringUtils.isBlank(json)) {
+//                log.error("未配置cid.config");
 //            }
-//            if(!runTaskFlow.equals(callees.getRunTaskFlow())){
-//                CompanyVoiceRoboticCallees updateCallees = new CompanyVoiceRoboticCallees();
-//                updateCallees.setId(callees.getId());
-//                updateCallees.setRunTaskFlow(runTaskFlow);
-//                companyVoiceRoboticCalleesMapper.updateById(updateCallees);
+//            CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
+//
+//            Notify notify = result.getNotify();
+//            String uuid = notify.getUuid();
+//
+//
+//            if (StringUtils.isNotBlank(uuid)) {
+//                getDialogMapDomain getDialogMap = getDialogMapDomain.builder()
+//                        .uuid(uuid)
+//                        .build();
+//                CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLog = companyVoiceRoboticCallLogCallphoneMapper.selectNoResultLogByCallees(callees);
+//
+//                companyVoiceRoboticCallLog.setStatus(2);
+//                companyVoiceRoboticCallLog.setResult(JSON.toJSONString(result));
+//
+//                CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callees.getRoboticId()).eq("customer_id", callees.getUserId()));
+//                CompanyVoiceRoboticWx roboticWx = companyVoiceRoboticWxServiceImpl.getById(companyWxClient.getRoboticWxId());
+//                Long setCompanyUserId = null;
+//                if(Integer.valueOf(1).equals(companyWxClient.getIsWeCom())){
+//                    CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
+//                    setCompanyUserId =  companyWxAccount.getCompanyUserId();
+//                }else if(Integer.valueOf(2).equals(companyWxClient.getIsWeCom())){
+//                    QwUser qwUser = qwUserMapper.selectById(roboticWx.getAccountId());
+//                    setCompanyUserId = qwUser.getCompanyUserId();
+//                }
+//
+//                companyVoiceRoboticCallLog.setCompanyUserId(setCompanyUserId);
+//                // 调用接口查询通话其他信息
+//                TaskInfo dialogMap = aiCallService.getDialogMapNew(getDialogMap, companyVoiceRoboticCallLog.getCompanyId());
+//                // 写入其他记录
+//                JSONObject telData = dialogMap.getTelData();
+//                companyVoiceRoboticCallLog.setRecordPath((String) telData.getOrDefault("recordPath", ""));
+//                companyVoiceRoboticCallLog.setContentList(telData.containsKey("contentList") ? telData.getJSONArray("contentList").toJSONString() : "");
+//                companyVoiceRoboticCallLog.setCallerNum((String) telData.getOrDefault("callerNum", ""));
+//                companyVoiceRoboticCallLog.setCalleeNum((String) telData.getOrDefault("calleeNum", ""));
+//                companyVoiceRoboticCallLog.setUuid((String) telData.getOrDefault("uuid", ""));
+//                Long createTime = telData.getLong("createTime");
+//                companyVoiceRoboticCallLog.setCallCreateTime(createTime);
+//                Long answerTime = telData.getLong("answerTime");
+//                companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
+//                companyVoiceRoboticCallLog.setIntention((String) telData.getOrDefault("intention", ""));
+//                companyVoiceRoboticCallLog.setCallTime(telData.getLong("duration"));
+//                BigDecimal callCharge = cidConfigVO.getCallCharge();
+//                //
+//                if (null == callCharge) {
+//                    callCharge = DEFAULT_CALL_CHARGE;
+//                }
+//                //向上取整分钟数
+//                BigDecimal divide = new BigDecimal(companyVoiceRoboticCallLog.getCallTime()).divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
+//                BigDecimal multiply = divide.multiply(callCharge);
+//                companyVoiceRoboticCallLog.setCost(multiply);
+//                baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
+//
+//                if (StringUtils.isNotBlank(notify.getUserData())) {
+//                    JSONObject userData = JSONObject.parseObject(redisCache2.getCacheObject(WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData()), JSONObject.class);
+//                    if (null != userData && userData.containsKey("callBackUuid") && userData.containsKey("workflowInstanceId") && userData.containsKey("nodeKey")) {
+//                        Map<String, Object> param = new HashMap<>();
+//                        param.put("callBackUuid", userData.getString("callBackUuid"));
+//                        param.put("callSource", "callBack");
+//                        CompletableFuture.runAsync(() -> {
+//                            companyWorkflowEngine.resumeFromBlockingNode(userData.getString("workflowInstanceId"), userData.getString("nodeKey"), param);
+//                        }, cidWorkFlowExecutor).thenRun(() -> {
+//                            redisCache2.deleteObject(WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData());
+//                        });
+//                    }
+//                    redisCache2.deleteObject(notify.getUserData());
+//                }
 //            }
-            String json = configService.selectConfigByKey("cId.config");
-            if (StringUtils.isBlank(json)) {
-                log.error("未配置cid.config");
-            }
-            CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
-
-            Notify notify = result.getNotify();
-            String uuid = notify.getUuid();
-
-
-            if (StringUtils.isNotBlank(uuid)) {
-                getDialogMapDomain getDialogMap = getDialogMapDomain.builder()
-                        .uuid(uuid)
-                        .build();
-                CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLog = companyVoiceRoboticCallLogCallphoneMapper.selectNoResultLogByCallees(callees);
-
-                companyVoiceRoboticCallLog.setStatus(2);
-                companyVoiceRoboticCallLog.setResult(JSON.toJSONString(result));
-
-                CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callees.getRoboticId()).eq("customer_id", callees.getUserId()));
-                CompanyVoiceRoboticWx roboticWx = companyVoiceRoboticWxServiceImpl.getById(companyWxClient.getRoboticWxId());
-                Long setCompanyUserId = null;
-                if(Integer.valueOf(1).equals(companyWxClient.getIsWeCom())){
-                    CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
-                    setCompanyUserId =  companyWxAccount.getCompanyUserId();
-                }else if(Integer.valueOf(2).equals(companyWxClient.getIsWeCom())){
-                    QwUser qwUser = qwUserMapper.selectById(roboticWx.getAccountId());
-                    setCompanyUserId = qwUser.getCompanyUserId();
-                }
-
-                companyVoiceRoboticCallLog.setCompanyUserId(setCompanyUserId);
-                // 调用接口查询通话其他信息
-                TaskInfo dialogMap = aiCallService.getDialogMapNew(getDialogMap, companyVoiceRoboticCallLog.getCompanyId());
-                // 写入其他记录
-                JSONObject telData = dialogMap.getTelData();
-                companyVoiceRoboticCallLog.setRecordPath((String) telData.getOrDefault("recordPath", ""));
-                companyVoiceRoboticCallLog.setContentList(telData.containsKey("contentList") ? telData.getJSONArray("contentList").toJSONString() : "");
-                companyVoiceRoboticCallLog.setCallerNum((String) telData.getOrDefault("callerNum", ""));
-                companyVoiceRoboticCallLog.setCalleeNum((String) telData.getOrDefault("calleeNum", ""));
-                companyVoiceRoboticCallLog.setUuid((String) telData.getOrDefault("uuid", ""));
-                Long createTime = telData.getLong("createTime");
-                companyVoiceRoboticCallLog.setCallCreateTime(createTime);
-                Long answerTime = telData.getLong("answerTime");
-                companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
-                companyVoiceRoboticCallLog.setIntention((String) telData.getOrDefault("intention", ""));
-                companyVoiceRoboticCallLog.setCallTime(telData.getLong("duration"));
-                BigDecimal callCharge = cidConfigVO.getCallCharge();
-                //
-                if (null == callCharge) {
-                    callCharge = DEFAULT_CALL_CHARGE;
-                }
-                //向上取整分钟数
-                BigDecimal divide = new BigDecimal(companyVoiceRoboticCallLog.getCallTime()).divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
-                BigDecimal multiply = divide.multiply(callCharge);
-                companyVoiceRoboticCallLog.setCost(multiply);
-                baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
-
-                if (StringUtils.isNotBlank(notify.getUserData())) {
-                    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"));
-                        param.put("callSource", "callBack");
-                        CompletableFuture.runAsync(() -> {
-                            companyWorkflowEngine.resumeFromBlockingNode(userData.getString("workflowInstanceId"), userData.getString("nodeKey"), param);
-                        }, cidWorkFlowExecutor).thenRun(() -> {
-                            redisCache2.deleteObject(WORKFLOW_CALL_ONE_REDIS_KEY + notify.getUserData());
-                        });
-                    }
-                    redisCache2.deleteObject(notify.getUserData());
-                }
-            }
-
-        } catch (Exception ex) {
-            log.error("处理回调结果异常:{}", result, ex);
-        }
+//
+//        } catch (Exception ex) {
+//            log.error("处理回调结果异常:{}", result, ex);
+//        }
     }
     @Async("callLogExcutor")
-    public void asyncHandleCalleeCallBackResult4EasyCall(EasyCallCallPhoneVO result, CompanyVoiceRoboticCallees callees) {
+    public void asyncHandleCalleeCallBackResult4EasyCall(EasyCallCallPhoneVO result, Long calleeId) {
         try {
-            String json = configService.selectConfigByKey("cId.config");
-            if (StringUtils.isBlank(json)) {
-                log.error("未配置cid.config");
-            }
-            CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
-            if (null != result) {
+
+            CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(calleeId);
+            try {
+                String json = configService.selectConfigByKey("cId.config");
+                if (StringUtils.isBlank(json)) {
+                    log.error("未配置cid.config");
+                }
+                CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
+                if (null != result) {
 //                getDialogMapDomain getDialogMap = getDialogMapDomain.builder()
 //                        .uuid(uuid)
 //                        .build();
-                CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLog = companyVoiceRoboticCallLogCallphoneMapper.selectNoResultLogByCallees(callees);
-
-                companyVoiceRoboticCallLog.setStatus(2);
-                companyVoiceRoboticCallLog.setResult(JSON.toJSONString(result));
-
-                CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callees.getRoboticId()).eq("customer_id", callees.getUserId()));
-                CompanyVoiceRoboticWx roboticWx = companyVoiceRoboticWxServiceImpl.getById(companyWxClient.getRoboticWxId());
-                Long setCompanyUserId = null;
-                if(Integer.valueOf(1).equals(companyWxClient.getIsWeCom())){
-                    CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
-                    setCompanyUserId =  companyWxAccount.getCompanyUserId();
-                }else if(Integer.valueOf(2).equals(companyWxClient.getIsWeCom())){
-                    QwUser qwUser = qwUserMapper.selectById(roboticWx.getAccountId());
-                    setCompanyUserId = qwUser.getCompanyUserId();
-                }
 
-                companyVoiceRoboticCallLog.setCompanyUserId(setCompanyUserId);
-                // 调用接口查询通话其他信息
+                    CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLog = companyVoiceRoboticCallLogCallphoneMapper.selectNoResultLogByCallees(callees);
+
+                    companyVoiceRoboticCallLog.setStatus(2);
+                    companyVoiceRoboticCallLog.setResult(JSON.toJSONString(result));
+                    companyVoiceRoboticCallLog.setHangupType(result.getUnconnectedReason());
+                    CompanyWxClient companyWxClient = companyWxClientServiceImpl.getOne(new QueryWrapper<CompanyWxClient>().eq("robotic_id", callees.getRoboticId()).eq("customer_id", callees.getUserId()));
+                    CompanyVoiceRoboticWx roboticWx = companyVoiceRoboticWxServiceImpl.getById(companyWxClient.getRoboticWxId());
+                    Long setCompanyUserId = null;
+                    if(null != roboticWx){
+                        if(Integer.valueOf(1).equals(companyWxClient.getIsWeCom())){
+                            CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(roboticWx.getAccountId());
+                            setCompanyUserId =  companyWxAccount.getCompanyUserId();
+                        }else if(Integer.valueOf(2).equals(companyWxClient.getIsWeCom())){
+                            QwUser qwUser = qwUserMapper.selectById(roboticWx.getAccountId());
+                            setCompanyUserId = qwUser.getCompanyUserId();
+                        }
+                    }else{
+                        CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(callees.getRoboticId());
+                        setCompanyUserId = robotic.getCompanyUserId();
+                    }
+
+
+                    companyVoiceRoboticCallLog.setCompanyUserId(setCompanyUserId);
+                    // 调用接口查询通话其他信息
 //                TaskInfo dialogMap = aiCallService.getDialogMapNew(getDialogMap, companyVoiceRoboticCallLog.getCompanyId());
-                // 写入其他记录
+                    // 写入其他记录
 //                JSONObject telData = dialogMap.getTelData();
-                companyVoiceRoboticCallLog.setRecordPath(result.getWavfile());
-                companyVoiceRoboticCallLog.setContentList(result.getDialogue());
-                companyVoiceRoboticCallLog.setCallerNum(result.getTelephone());
-                companyVoiceRoboticCallLog.setCalleeNum(result.getCallerNumber());
-                companyVoiceRoboticCallLog.setUuid(result.getUuid());
-                Long createTime = result.getCalloutTime();
-                companyVoiceRoboticCallLog.setCallCreateTime(createTime);
-                Long answerTime = result.getCallEndTime();
-                companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
-                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);
+                    companyVoiceRoboticCallLog.setRecordPath(result.getWavfile());
+                    companyVoiceRoboticCallLog.setCallType(2);
+                    companyVoiceRoboticCallLog.setContentList(result.getDialogue());
+                    companyVoiceRoboticCallLog.setCallerNum(PhoneUtil.encryptPhone(result.getTelephone()));
+                    companyVoiceRoboticCallLog.setCalleeNum(result.getCallerNumber());
+                    companyVoiceRoboticCallLog.setUuid(result.getUuid());
+                    Long createTime = result.getCalloutTime();
+                    companyVoiceRoboticCallLog.setCallCreateTime(createTime);
+                    Long answerTime = result.getCallEndTime();
+                    companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
+                    // 【当前启用】读取 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(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;
                     }
-                }
-                // 历史第三方值(当前不启用,保留用于回滚)
-                // String intention = result.getIntent();
-                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();
+                    if (StringUtils.isBlank(intentf)) {
+                        intentf = "0";
                     }
-                } else {
-                    intentf = intentionLabel;
-                }
-                if (StringUtils.isBlank(intentf)) intentf = "0";
-                companyVoiceRoboticCallLog.setIntention(intentf);
-                companyVoiceRoboticCallLog.setCallTime(Long.valueOf(result.getTimeLen()/1000));
-                BigDecimal callCharge = cidConfigVO.getCallCharge();
-                //
-                if (null == callCharge) {
-                    callCharge = DEFAULT_CALL_CHARGE;
-                }
-                //向上取整分钟数
-                BigDecimal divide = new BigDecimal(companyVoiceRoboticCallLog.getCallTime()).divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
-                BigDecimal multiply = divide.multiply(callCharge);
-                companyVoiceRoboticCallLog.setCost(multiply);
-                baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
-                // 更新用户标签
-                crmCustomerPropertyService.addPropertyByCallLog(companyVoiceRoboticCallLog);
-
-                if (StringUtils.isNotBlank(result.getBizJson())) {
-                    JSONObject bizJson = JSONObject.parseObject(result.getBizJson());
-                    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"));
-                        param.put("callSource", "callBack");
-                        CompletableFuture.runAsync(() -> {
-                            companyWorkflowEngine.resumeFromBlockingNode(userData.getString("workflowInstanceId"), userData.getString("nodeKey"), param);
-                        }, cidWorkFlowExecutor).thenRun(() -> {
-                            redisCache2.deleteObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid"));
-                        });
+                    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());
+                    }
+                    BigDecimal callCharge = cidConfigVO.getCallCharge();
+                    //
+                    if (null == callCharge) {
+                        callCharge = DEFAULT_CALL_CHARGE;
+                    }
+                    //检测空避免报错
+                    if(companyVoiceRoboticCallLog.getCallTime()!=null){
+                        //向上取整分钟数
+                        BigDecimal divide = new BigDecimal(companyVoiceRoboticCallLog.getCallTime()).divide(ONE_MINUTES_SECOND, 0, RoundingMode.CEILING);
+                        BigDecimal multiply = divide.multiply(callCharge);
+                        companyVoiceRoboticCallLog.setCost(multiply);
+                    }
+
+                    //检测敏感词语
+                    AgentSensitiveWordDetectResultDTO resultDTO = agentSensitiveWordDetector.detectAndHighlight(result.getUuid(), result.getDialogue());
+                    if(resultDTO != null && resultDTO.getCheckBoolean()){
+                        companyVoiceRoboticCallLog.setIsWarning(1);
+                        companyVoiceRoboticCallLog.setContentList(resultDTO.getProcessText());
+                        companyVoiceRoboticCallLog.setViolationNum(resultDTO.getViolationCount());
+                    }
+
+                    //AI外呼 转人工逻辑
+                    if(StringUtils.isNotBlank(result.getAcdOpnum()) && !"robot".equalsIgnoreCase(result.getAcdOpnum())){
+                        companyVoiceRoboticCallLog.setManualAnswered(1);
+                        companyVoiceRoboticCallLog.setHandleFlag(0);
+                        companyVoiceRoboticCallLog.setAnsweredExtNum(result.getAcdOpnum());
+                        companyVoiceRoboticCallLog.setManualAnsweredTime(result.getManualAnsweredTime());
+                        companyVoiceRoboticCallLog.setManualAnsweredTimeLen(result.getManualAnsweredTimeLen());
+                    }
+
+                    baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
+                    // 更新用户标签
+                    crmCustomerPropertyService.addPropertyByCallLog(companyVoiceRoboticCallLog);
+
+                    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);
+                        if (null != userData && userData.containsKey("callBackUuid") && userData.containsKey("workflowInstanceId") && userData.containsKey("nodeKey")) {
+                            // 暂停检查:任务暂停中则标记回调已到达但不触发工作流继续执行
+                            if (companyVoiceRoboticService.isTaskPaused(callees.getRoboticId())) {
+                                log.info("任务暂停中,EasyCall外呼回调数据已保存但不触发工作流继续执行 - taskId: {}, workflowInstanceId: {}",
+                                        callees.getRoboticId(), userData.getString("workflowInstanceId"));
+                                markCallbackReceivedInVariables(userData.getString("workflowInstanceId"));
+                            } else {
+                                Map<String, Object> param = new HashMap<>();
+                                param.put("callBackUuid", userData.getString("callBackUuid"));
+                                param.put("callSource", "callBack");
+                                CompletableFuture.runAsync(() -> {
+                                    companyWorkflowEngine.resumeFromBlockingNode(userData.getString("workflowInstanceId"), userData.getString("nodeKey"), param);
+                                }, cidWorkFlowExecutor).thenRun(() -> {
+                                    redisCache2.deleteObject(EASYCALL_WORKFLOW_REDIS_KEY +  bizJson.getString("callBackUuid"));
+                                });
+                            }
+                        }
+                        redisCache2.deleteObject(bizJson.getString("callBackUuid"));
                     }
-                    redisCache2.deleteObject(bizJson.getString("callBackUuid"));
                 }
+
+            } catch (Exception ex) {
+                log.error("处理回调结果异常:{}", result, ex);
             }
 
-        } catch (Exception ex) {
-            log.error("处理回调结果异常:{}", result, ex);
+        } catch (Exception e) {
+            log.error("asyncHandleCalleeCallBackResult4EasyCall执行失败", e);
+        }
+
+    }
+
+    /**
+     * 标记工作流实例的variables中回调已到达(任务暂停时使用,恢复时会检查此标记)
+     */
+    private void markCallbackReceivedInVariables(String workflowInstanceId) {
+        try {
+            CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
+            if (exec == null) {
+                log.warn("markCallbackReceived: 未找到工作流实例 - workflowInstanceId: {}", workflowInstanceId);
+                return;
+            }
+            String variablesJson = exec.getVariables();
+            JSONObject variables;
+            if (StringUtils.isNotBlank(variablesJson)) {
+                variables = JSONObject.parseObject(variablesJson);
+            } else {
+                variables = new JSONObject();
+            }
+            variables.put("pause_callback_received", true);
+            CompanyAiWorkflowExec update = new CompanyAiWorkflowExec();
+            update.setWorkflowInstanceId(workflowInstanceId);
+            update.setVariables(variables.toJSONString());
+            companyAiWorkflowExecMapper.updateByWorkflowInstanceId(update);
+            log.info("markCallbackReceived: 已标记回调到达 - workflowInstanceId: {}", workflowInstanceId);
+        } catch (Exception e) {
+            log.error("markCallbackReceived: 标记回调到达失败 - workflowInstanceId: {}", workflowInstanceId, e);
         }
     }
 
@@ -393,58 +516,119 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     }
 
     @Override
-    public List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId) {
-        if (customerIds == null || customerIds.isEmpty()) {
-            return Collections.emptyList();
-        }
-        return baseMapper.countRoboticCallOutByCustomerIds(customerIds, companyId);
+    public List<Long> getCallerIdsByCustomerId(Long customerId) {
+        return companyVoiceRoboticCalleesMapper.getCallerIdsByCustomerId(customerId);
     }
 
     @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);
+    public List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
     }
-    /**
-     * Redis 中 workflow 回调缓存可能是 String 或已反序列化的 JSONObject,避免 ClassCastException。
-     */
-    private static JSONObject parseRedisCacheToJsonObject(Object cacheObj) {
-        if (cacheObj == null) {
-            return null;
-        }
-        if (cacheObj instanceof JSONObject) {
-            return (JSONObject) cacheObj;
+
+    @Override
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount(Long companyId,Long companyUserId) {
+        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogCount(companyId,companyUserId);
+    }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        return baseMapper.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+    }
+
+
+
+    @Override
+    public void prepareDetailListQuery(CompanyVoiceRoboticCallLogCallphone query, Long companyId) {
+        query.setCompanyId(companyId);
+        prepareDetailPhoneQuery(query);
+    }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogCallPhoneVO> selectDetailList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = baseMapper.selectDetailList(companyVoiceRoboticCallLogCallphone);
+        if (list == null || list.isEmpty()) {
+            return list;
         }
-        if (cacheObj instanceof String) {
-            String s = (String) cacheObj;
-            if (StringUtils.isBlank(s)) {
-                return null;
+        for (CompanyVoiceRoboticCallLogCallPhoneVO vo : list) {
+            if (StringUtils.isNotEmpty(vo.getCallerNum())) {
+                vo.setCallerNum(PhoneUtil.decryptPhoneMk(vo.getCallerNum()));
             }
-            return JSONObject.parseObject(s);
         }
-        return JSONObject.parseObject(JSONObject.toJSONString(cacheObj));
+        return list;
     }
 
     @Override
-    public List<Long> getCallerIdsByCustomerId(Long customerId) {
-        return companyVoiceRoboticCalleesMapper.getCallerIdsByCustomerId(customerId);
+    public CompanyVoiceRoboticCallLogDetailSummary selectDetailSummary(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        CompanyVoiceRoboticCallLogDetailSummary summary = baseMapper.selectDetailSummary(companyVoiceRoboticCallLogCallphone);
+        if (summary == null) {
+            summary = new CompanyVoiceRoboticCallLogDetailSummary();
+            summary.setTotalCount(0L);
+            summary.setSuccessCount(0L);
+            summary.setFailCount(0L);
+            summary.setConnectedCount(0L);
+            summary.setTotalBillingMinute(0L);
+        }
+        if (summary.getTotalBillingMinute() == null) {
+            summary.setTotalBillingMinute(0L);
+        }
+        long total = summary.getTotalCount() == null ? 0L : summary.getTotalCount();
+        long connected = summary.getConnectedCount() == null ? 0L : summary.getConnectedCount();
+        summary.setConnectRate(total > 0 ? (int) Math.round(connected * 100.0 / total) : 0);
+        return summary;
     }
 
     @Override
-    public List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
-        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
+    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;
     }
 
-    @Override
-    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount() {
-        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogCount();
+    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;
     }
 
-    @Override
-    public List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
-        return baseMapper.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+    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()));
+        }
+        if (StringUtils.isNotEmpty(query.getIntention()) && "0".equals(query.getIntention())) {
+            query.setIntentionEmpty(true);
+            query.setIntention(null);
+        } else {
+            query.setIntentionEmpty(null);
+        }
     }
     /**
      * 判断整数
@@ -455,4 +639,51 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     public boolean isPositiveInteger(String str) {
         return str != null && str.matches("[1-9]\\d*");
     }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogCallphone> selectManualAnsweredList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        return baseMapper.selectManualAnsweredList(companyVoiceRoboticCallLogCallphone);
+    }
+
+    @Override
+    public int markHandleFlag(Long logId) {
+        CompanyVoiceRoboticCallLogCallphone updateObj = new CompanyVoiceRoboticCallLogCallphone();
+        updateObj.setLogId(logId);
+        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;
+    }
+
+    @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);
+    }
+
+    @Override
+    public List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return baseMapper.countRoboticCallOutByCustomerIds(customerIds, companyId);
+    }
 }

+ 15 - 16
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogSendmsgServiceImpl.java

@@ -1,24 +1,23 @@
 package com.fs.company.service.impl;
 
-import java.util.Collections;
-import java.util.List;
-import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
 import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogSendmsgMapper;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogSendmsgService;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogSendmsgMapper;
-import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
-import com.fs.company.service.ICompanyVoiceRoboticCallLogSendmsgService;
+
+import java.util.List;
 
 /**
  * 调用日志_发送短信Service业务层处理
- * 
+ *
  * @author fs
  * @date 2026-01-15
  */
@@ -30,7 +29,7 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
     CompanyVoiceRoboticBusinessMapper companyVoiceRoboticBusinessMapper;
     /**
      * 查询调用日志_发送短信
-     * 
+     *
      * @param logId 调用日志_发送短信主键
      * @return 调用日志_发送短信
      */
@@ -47,7 +46,7 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
 
     /**
      * 查询调用日志_发送短信列表
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 调用日志_发送短信
      */
@@ -59,7 +58,7 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
 
     /**
      * 新增调用日志_发送短信
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 结果
      */
@@ -72,7 +71,7 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
 
     /**
      * 修改调用日志_发送短信
-     * 
+     *
      * @param companyVoiceRoboticCallLogSendmsg 调用日志_发送短信
      * @return 结果
      */
@@ -84,7 +83,7 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
 
     /**
      * 批量删除调用日志_发送短信
-     * 
+     *
      * @param logIds 需要删除的调用日志_发送短信主键
      * @return 结果
      */
@@ -96,7 +95,7 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
 
     /**
      * 删除调用日志_发送短信信息
-     * 
+     *
      * @param logId 调用日志_发送短信主键
      * @return 结果
      */
@@ -127,7 +126,7 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
     }
 
     @Override
-    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount() {
-        return baseMapper.selectCompanyVoiceRoboticCallLogSendMsgCount();
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount(Long companyId,Long companyUserId) {
+        return baseMapper.selectCompanyVoiceRoboticCallLogSendMsgCount( companyId, companyUserId);
     }
 }

+ 3 - 4
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCalleesServiceImpl.java

@@ -95,10 +95,9 @@ public class CompanyVoiceRoboticCalleesServiceImpl extends ServiceImpl<CompanyVo
 
     @Override
     public List<CompanyVoiceRoboticCallees> selectCompanyVoiceRoboticCalleesListByRoboticId(Long id) {
-//        CompanyVoiceRoboticCallees param = new CompanyVoiceRoboticCallees();
-//        param.setRoboticId(id);
-//        List<CompanyVoiceRoboticCallees> companyVoiceRoboticCallees = selectCompanyVoiceRoboticCalleesList(param);
-        List<CompanyVoiceRoboticCallees> companyVoiceRoboticCallees = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesListByRoboticId(id);
+        CompanyVoiceRoboticCallees param = new CompanyVoiceRoboticCallees();
+        param.setRoboticId(id);
+        List<CompanyVoiceRoboticCallees> companyVoiceRoboticCallees = selectCompanyVoiceRoboticCalleesList(param);
         if(null != companyVoiceRoboticCallees && !companyVoiceRoboticCallees.isEmpty()){
             companyVoiceRoboticCallees.forEach(item -> {
                 item.setIdToString(item.getId().toString());

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 429 - 350
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java


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

@@ -12,7 +12,7 @@ import java.util.List;
 
 /**
  * AI外呼任务加微方式Service业务层处理
- * 
+ *
  * @author fs
  * @date 2024-12-06
  */
@@ -24,7 +24,7 @@ public class CompanyVoiceRoboticWxServiceImpl extends ServiceImpl<CompanyVoiceRo
 
     /**
      * 查询AI外呼任务加微方式
-     * 
+     *
      * @param id AI外呼任务加微方式ID
      * @return AI外呼任务加微方式
      */
@@ -36,7 +36,7 @@ public class CompanyVoiceRoboticWxServiceImpl extends ServiceImpl<CompanyVoiceRo
 
     /**
      * 查询AI外呼任务加微方式列表
-     * 
+     *
      * @param companyVoiceRoboticWx AI外呼任务加微方式
      * @return AI外呼任务加微方式
      */
@@ -48,7 +48,7 @@ public class CompanyVoiceRoboticWxServiceImpl extends ServiceImpl<CompanyVoiceRo
 
     /**
      * 新增AI外呼任务加微方式
-     * 
+     *
      * @param companyVoiceRoboticWx AI外呼任务加微方式
      * @return 结果
      */
@@ -61,7 +61,7 @@ public class CompanyVoiceRoboticWxServiceImpl extends ServiceImpl<CompanyVoiceRo
 
     /**
      * 修改AI外呼任务加微方式
-     * 
+     *
      * @param companyVoiceRoboticWx AI外呼任务加微方式
      * @return 结果
      */
@@ -73,7 +73,7 @@ public class CompanyVoiceRoboticWxServiceImpl extends ServiceImpl<CompanyVoiceRo
 
     /**
      * 批量删除AI外呼任务加微方式
-     * 
+     *
      * @param ids 需要删除的AI外呼任务加微方式ID
      * @return 结果
      */
@@ -85,7 +85,7 @@ public class CompanyVoiceRoboticWxServiceImpl extends ServiceImpl<CompanyVoiceRo
 
     /**
      * 删除AI外呼任务加微方式信息
-     * 
+     *
      * @param id AI外呼任务加微方式ID
      * @return 结果
      */

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

@@ -1,13 +1,13 @@
 package com.fs.company.service.impl;
 
-import java.util.List;
-
+import com.fs.company.domain.CompanyVoice;
+import com.fs.company.mapper.CompanyVoiceMapper;
+import com.fs.company.service.ICompanyVoiceService;
 import com.fs.company.vo.CompanyVoiceListVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyVoiceMapper;
-import com.fs.company.domain.CompanyVoice;
-import com.fs.company.service.ICompanyVoiceService;
+
+import java.util.List;
 
 /**
  * 企业通话Service业务层处理

+ 20 - 133
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.time.LocalTime;
@@ -78,10 +77,10 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
     CompanyWxClientMapper companyWxClientMapper;
 
     @Autowired
-    private CloudHostProper cloudHostProper;
+    private RedisCache redisCache;
 
     @Autowired
-    private RedisCache redisCache;
+    private CloudHostProper cloudHostProper;
 
     /**
      * 初始化工作流
@@ -90,7 +89,7 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
     @Override
     public ExecutionResult initialize(Long workflowDefinitionId, Map<String, Object> inputVariables) {
         try {
-            if(!inputVariables.containsKey("cidGroupNo")){
+            if (!inputVariables.containsKey("cidGroupNo")) {
                 throw new CustomException("参数错误:未指定cidGroupNo");
             }
             // 生成工作流实例ID
@@ -305,9 +304,9 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
             currentExec.setBusinessKey(context.getBusinessId());
             currentExec.setStartNodeKey(workflow.getStartNodeKey());
             currentExec.setEndNodeKey(workflow.getEndNodeKey());
-            currentExec.setCidGroupNo(context.getVariable("cidGroupNo",Integer.class));
+            currentExec.setCidGroupNo(context.getVariable("cidGroupNo", Integer.class));
             currentExec.setRuntimeRangeStart(context.getVariable("runtimeRangeStart", LocalTime.class));
-            currentExec.setRuntimeRangeEnd(context.getVariable("runtimeRangeEnd",LocalTime.class));
+            currentExec.setRuntimeRangeEnd(context.getVariable("runtimeRangeEnd", LocalTime.class));
             currentExecutionMapper.insert(currentExec);
         } catch (JsonProcessingException e) {
             throw new RuntimeException(e);
@@ -461,8 +460,8 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
 
             // 检查当前工作流是否处于暂停状态
             if (!Integer.valueOf(ExecutionStatusEnum.PAUSED.getValue()).equals(currentExec.getStatus()) &&
-                !Integer.valueOf(ExecutionStatusEnum.WAITING.getValue()).equals(currentExec.getStatus()) &&
-                !Integer.valueOf(ExecutionStatusEnum.WAITING_DO_CALL.getValue()).equals(currentExec.getStatus())) {
+                    !Integer.valueOf(ExecutionStatusEnum.WAITING.getValue()).equals(currentExec.getStatus()) &&
+                    !Integer.valueOf(ExecutionStatusEnum.WAITING_DO_CALL.getValue()).equals(currentExec.getStatus())) {
                 throw new CustomException("工作流未处于暂停状态,无法唤醒: " + workflowInstanceId);
             }
 
@@ -499,6 +498,7 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
      * @param inputData
      */
     @Override
+    @Async("cidWorkFlowExecutor")
     public void timeDoExecute(String workflowInstanceId, String nodeKey, Map<String, Object> inputData) {
         try {
             // 加载当前执行记录
@@ -563,7 +563,7 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
         }
     }
 
-    public void workFlowAddExec(){
+    public void workFlowAddExec() {
 
     }
 
@@ -573,7 +573,6 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
      * @param workFlowId
      */
     public Long createSipTask(Long roboticId, Long workFlowId) {
-
         List<String> nodeTypes = Arrays.asList(NodeTypeEnum.AI_CALL_TASK.getCode());
         CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticId);
         List<CompanyWorkflowNode> companyWorkflowNodes = companyWorkflowNodeMapper.selectNodesByWorkflowIdAndTypes(workFlowId, nodeTypes);
@@ -647,132 +646,20 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
             }
         }
         return null;
-
-//        try {
-//            List<String> nodeTypes = Arrays.asList(NodeTypeEnum.AI_CALL_TASK.getCode());
-//            CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticId);
-//            List<CompanyWorkflowNode> companyWorkflowNodes = companyWorkflowNodeMapper.selectNodesByWorkflowIdAndTypes(workFlowId, nodeTypes);
-//            //为所有外呼节点创建任务的对应sip外呼任务
-//            for (CompanyWorkflowNode callNode : companyWorkflowNodes) {
-//                String nodeConfig = callNode.getNodeConfig();
-//                AiCallConfigVO callConfigVo = JSONObject.parseObject(nodeConfig, AiCallConfigVO.class);
-//                EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
-//                // 任务名称:使用任务名称_工作流id_节点key
-//                createParam.setBatchName(cloudHostProper.getCompanyName()+"-"+robotic.getName() + "_" + workFlowId + "_" + callNode.getNodeKey());
-//                if (null != callConfigVo.getMaxConcurrency()) {
-//                    createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
-//                } else {
-//                    createParam.setThreadNum(3L);
-//                }
-//                // AI 外呼模式
-//                createParam.setTaskType(1);
-//                // 外呼线路(网关)
-//                createParam.setGatewayId(callConfigVo.getGatewayId());
-//                // 大模型底座
-//                createParam.setLlmAccountId(callConfigVo.getLlmAccountId());
-//                // 音色编号
-//                createParam.setVoiceCode(callConfigVo.getVoiceCode());
-//                // 音色来源(如未配置默认留空,由 EasyCallCenter365 使用默认值)
-//                createParam.setVoiceSource(callConfigVo.getVoiceSource());
-//                // 技能组(转人工客服分组,可选)
-//                createParam.setGroupId(callConfigVo.getBusiGroupId());
-//                // 模型参数
-//                createParam.setTtsModels(callConfigVo.getTtsModels());
-//
-//                EasyCallTaskVO task = easyCallService.createTask(createParam, null);
-//                if (task == null || task.getBatchId() == null) {
-//                    log.error("createSipTask: 创建 EasyCall 任务失败 - workflowInstanceId: {}", workFlowId);
-//                    throw new RuntimeException("EasyCallCenter365 创建任务失败");
-//                }
-//                CompanySiptaskInfo sipTaskInfo = new CompanySiptaskInfo();
-//                sipTaskInfo.setTaskId(roboticId);
-//                sipTaskInfo.setWorkflowId(workFlowId);
-//                sipTaskInfo.setNodeKey(callNode.getNodeKey());
-//                sipTaskInfo.setBatchId(task.getBatchId());
-//                sipTaskInfo.setTaskJson(JSONObject.toJSONString(task));
-//                companySiptaskInfoMapper.insertCompanySiptaskInfo(sipTaskInfo);
-//                return task.getBatchId();
-//            }
-//        } catch (Exception ex) {
-//            log.error("创建SIP任务失败:{}", ex);
-//        }
-//        return null;
     }
 
-    /**
-     * 加微成功后流程唤醒操作
-     * @param object
-     */
-    @Async("cidWorkFlowExecutor")
-    public void addWxSuccess(JSONObject object){
-
-        String workflowInstanceId = object.getString("instanceId");
-        String nodeKey = object.getString("nodeKey");
-        Long accountId = object.getLong("accountId");
-        String remark = object.getString("remark");
-        if(StringUtils.isBlank(remark) || StringUtils.isBlank(workflowInstanceId) || StringUtils.isBlank(nodeKey) || accountId == null){
-            log.error("addWxSuccess: 参数错误,workflowInstanceId:{},nodeKey:{},accountId:{},remark:{}", workflowInstanceId, nodeKey, accountId, remark);
-            return;
-        }
-        CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(accountId);
-        if(null != companyWxAccount && !remark.equals(companyWxAccount.getRemark())){
-            companyWxAccount.setRemark( remark);
-            companyWxAccountMapper.updateCompanyWxAccount(companyWxAccount);
-        }
-
-        // 加载当前执行记录
-        CompanyAiWorkflowExec currentExec = currentExecutionMapper.selectByWorkflowInstanceId(workflowInstanceId);
-
-        if (currentExec == null) {
-            throw new CustomException("工作流实例不存在: " + workflowInstanceId);
-        }
-        //更新加微结果到wxClient表
-        updateWxClientSuccessByWorkflowInstanceId(workflowInstanceId);
-        // 验证当前节点是否匹配
-        if (!nodeKey.equals(currentExec.getCurrentNodeKey())) {
-            log.error("节点不匹配 - 期望: {}, 实际: {}", nodeKey, currentExec.getCurrentNodeKey());
-            return;
-        }
-
-        // 检查当前工作流是否处于暂停状态
-        if (!Integer.valueOf(ExecutionStatusEnum.PAUSED.getValue()).equals(currentExec.getStatus()) &&
-                !Integer.valueOf(ExecutionStatusEnum.WAITING.getValue()).equals(currentExec.getStatus())) {
-            log.error("工作流未处于暂停状态,无法唤醒::{} " , workflowInstanceId);
-        }
-
-        // 反序列化执行上下文并合并新的输入数据
-        ExecutionContext context = deserializeContext(currentExec);
-
-        // 加载工作流定义
-        CompanyWorkflow definition = loadCompanyWorkflow(currentExec.getWorkflowId());
-
-        // 创建节点实例
-        IWorkflowNode node = createNode(definition, nodeKey);
-        if (node == null) {
-            throw new CustomException("节点不存在: " + nodeKey);
-        }
-
-        // 继续执行节点逻辑
-        ExecutionResult result = node.continueExecute(context);
-
-    }
-
-    /**
-     * 更新加微结果到wxClient表
-     * @param workflowInstanceId
-     */
-    public void updateWxClientSuccessByWorkflowInstanceId(String workflowInstanceId){
-        Long id = currentExecutionMapper.selectWxClientIdByWorkflowInstanceId(workflowInstanceId);
-        CompanyWxClient client = companyWxClientMapper.selectCompanyWxClientById(id);
-        if(null != client && !Integer.valueOf(1).equals(client.getIsAdd())){
-            //更新wxClient表
-            CompanyWxClient companyWxClient = new CompanyWxClient();
-            companyWxClient.setId(id);
-            companyWxClient.setIsAdd(1);
-            companyWxClient.setSuccessAddTime(LocalDateTime.now());
-            companyWxClientMapper.updateCompanyWxClient(companyWxClient);
+    @Override
+    public void updateExecVariables(String workflowInstanceId, Map<String, Object> variables) {
+        try {
+            CompanyAiWorkflowExec update = new CompanyAiWorkflowExec();
+            update.setWorkflowInstanceId(workflowInstanceId);
+            update.setVariables(objectMapper.writeValueAsString(variables));
+            update.setLastUpdateTime(LocalDateTime.now());
+            currentExecutionMapper.updateByWorkflowInstanceId(update);
+            log.info("更新exec variables成功 - workflowInstanceId: {}", workflowInstanceId);
+        } catch (Exception e) {
+            log.error("更新exec variables失败 - workflowInstanceId: {}", workflowInstanceId, e);
         }
-
     }
 
 }

+ 25 - 6
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java

@@ -1,10 +1,8 @@
 package com.fs.company.service.impl;
 
 import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
-import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
@@ -368,7 +366,9 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
     }
 
     private String getWorkflowTypeName(Integer workflowType) {
-        if (workflowType == null) return "";
+        if (workflowType == null) {
+            return "";
+        }
         switch (workflowType) {
             case 1: return "对话流程";
             case 2: return "任务流程";
@@ -435,9 +435,13 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
 
     @Override
     public AjaxResult updateWorkflowBindCompanyUser(CompanyWorkflowUpdateBindWCParam param) {
-        if (param == null ||param.getWorkflowId() == null || param.getCompanyUserIds() == null || param.getCompanyUserIds().size() == 0) return AjaxResult.error("传参异常");
+        if (param == null ||param.getWorkflowId() == null || param.getCompanyUserIds() == null || param.getCompanyUserIds().size() == 0) {
+            return AjaxResult.error("传参异常");
+        }
         CompanyWorkflow fsAiWorkflowResult = companyWorkflowMapper.selectCompanyWorkflowById(Long.valueOf(param.getWorkflowId().toString()));
-        if (fsAiWorkflowResult == null)return AjaxResult.error("未查询到对应工作流,请刷新后重试");
+        if (fsAiWorkflowResult == null) {
+            return AjaxResult.error("未查询到对应工作流,请刷新后重试");
+        }
         int i = companyWorkflowMapper.insertCompanyWorkflowCompanyUser(param.getWorkflowId(),param.getCompanyUserIds());
         List<CompanyWorkflowNode> fsAiWorkflowNodes = companyWorkflowNodeMapper.selectNodesByWorkflowIdAndTypes(param.getWorkflowId(), aiCallNodeTypes);
         companyWorkflowMapper.insertAiWorkflowCompanyUserVoice(param.getWorkflowId(), param.getCompanyUserIds(), fsAiWorkflowNodes);
@@ -447,7 +451,9 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
     @Override
     public AjaxResult getBindCompanyUserByWorkflowId(Long workflowId) {
         CompanyWorkflow fsAiWorkflow = companyWorkflowMapper.selectCompanyWorkflowById(workflowId);
-        if (fsAiWorkflow == null) return AjaxResult.error("工作流不存在");
+        if (fsAiWorkflow == null) {
+            return AjaxResult.error("工作流不存在");
+        }
         return AjaxResult.success(companyWorkflowMapper.getBindCompanyUserByWorkflowId(workflowId));
     }
 
@@ -694,4 +700,17 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
 
         return workflowId;
     }
+
+    @Override
+    public List<String> selectNodeTypeCodesByWorkflowId(Long workflowId) {
+        List<CompanyWorkflowNode> nodes = companyWorkflowNodeMapper.selectCompanyWorkflowNodeByWorkflowId(workflowId);
+        if (nodes == null || nodes.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return nodes.stream()
+                .map(CompanyWorkflowNode::getNodeType)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+    }
 }

+ 12 - 12
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxChatServiceImpl.java

@@ -1,18 +1,18 @@
 package com.fs.company.service.impl;
 
-import java.util.List;
-
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.utils.DateUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyWxChatMapper;
 import com.fs.company.domain.CompanyWxChat;
+import com.fs.company.mapper.CompanyWxChatMapper;
 import com.fs.company.service.ICompanyWxChatService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
 
 /**
  * 个微聊天Service业务层处理
- * 
+ *
  * @author fs
  * @date 2025-01-15
  */
@@ -24,7 +24,7 @@ public class CompanyWxChatServiceImpl extends ServiceImpl<CompanyWxChatMapper, C
 
     /**
      * 查询个微聊天
-     * 
+     *
      * @param id 个微聊天主键
      * @return 个微聊天
      */
@@ -36,7 +36,7 @@ public class CompanyWxChatServiceImpl extends ServiceImpl<CompanyWxChatMapper, C
 
     /**
      * 查询个微聊天列表
-     * 
+     *
      * @param companyWxChat 个微聊天
      * @return 个微聊天
      */
@@ -48,7 +48,7 @@ public class CompanyWxChatServiceImpl extends ServiceImpl<CompanyWxChatMapper, C
 
     /**
      * 新增个微聊天
-     * 
+     *
      * @param companyWxChat 个微聊天
      * @return 结果
      */
@@ -61,7 +61,7 @@ public class CompanyWxChatServiceImpl extends ServiceImpl<CompanyWxChatMapper, C
 
     /**
      * 修改个微聊天
-     * 
+     *
      * @param companyWxChat 个微聊天
      * @return 结果
      */
@@ -74,7 +74,7 @@ public class CompanyWxChatServiceImpl extends ServiceImpl<CompanyWxChatMapper, C
 
     /**
      * 批量删除个微聊天
-     * 
+     *
      * @param ids 需要删除的个微聊天主键
      * @return 结果
      */
@@ -86,7 +86,7 @@ public class CompanyWxChatServiceImpl extends ServiceImpl<CompanyWxChatMapper, C
 
     /**
      * 删除个微聊天信息
-     * 
+     *
      * @param id 个微聊天主键
      * @return 结果
      */

+ 1 - 6
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java

@@ -243,12 +243,7 @@ public class CompanyWxClientServiceImpl extends ServiceImpl<CompanyWxClientMappe
     }
 
     @Override
-    public List<CompanyWxClient4WorkFlowVO> getAddWxList4WorkflowNew(List<Long> accountIdList, Integer cidGroupId){
-        return baseMapper.getAddWxList4Workflow(accountIdList, ExecutionStatusEnum.PAUSED.getValue(), NodeTypeEnum.AI_ADD_WX_TASK_NEW.getValue(), cidGroupId);
-    }
-
-    @Override
-    public List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList, Integer isWeCom , Integer cidGroupNo) {
+    public List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList, Integer isWeCom,Integer cidGroupNo) {
         return baseMapper.getQwAddWxList(accountIdList,isWeCom,cidGroupNo);
     }
 

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxDialogServiceImpl.java

@@ -55,7 +55,7 @@ public class CompanyWxDialogServiceImpl extends ServiceImpl<CompanyWxDialogMappe
     public int insertCompanyWxDialog(CompanyWxDialog companyWxDialog)
     {
         companyWxDialog.setCreateTime(DateUtils.getNowDate());
-        return companyWxDialogMapper.insert(companyWxDialog);
+        return companyWxDialogMapper.insertCompanyWxDialog(companyWxDialog);
     }
 
     /**

+ 14 - 17
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java

@@ -1,8 +1,6 @@
 package com.fs.company.service.impl;
 
 import cn.hutool.http.HttpUtil;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -13,7 +11,6 @@ import com.fs.common.utils.DateUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.service.*;
-import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
 import com.fs.company.service.impl.call.node.AiAddWxTaskNewNode;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
@@ -24,7 +21,6 @@ import com.fs.wxcid.dto.friend.ContactItem;
 import com.fs.wxcid.dto.friend.ContactListResponse;
 import com.fs.wxcid.dto.friend.GetContactDetailsListParam;
 import com.fs.wxcid.dto.friend.GetContactDetailsResponseData;
-import com.fs.wxcid.dto.friend.GetContactListParam;
 import com.fs.wxcid.dto.login.AccountIdParam;
 import com.fs.wxcid.dto.login.GetLoginQrCodeParam;
 import com.fs.wxcid.dto.login.LoginStatusResponseData;
@@ -38,8 +34,6 @@ import com.fs.wxcid.service.*;
 import com.fs.wxcid.service.impl.LoginServiceImpl;
 import com.fs.wxcid.service.impl.UserServiceImpl;
 import lombok.extern.slf4j.Slf4j;
-import okhttp3.*;
-import okhttp3.WebSocketListener;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
@@ -114,8 +108,6 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
 
     @Value("${wx.api.url:http://localhost:7113}")
     private String wxApiUrl;
-
-
     /**
      * 查询企微账号
      *
@@ -282,11 +274,17 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
         if (count==null){
             return  R.error(501,"回调服务器不足,请联系管理员");
         }
+//        CompanyAiWorkflowServer cidServer = companyAiWorkflowServerMapper.selectCompanyAiWorkflowServerById(companyUser.getCidServerId());
+//
+//        Long serverId = cidIpadServerService.selectQwIpadServerByAddressId(addressId,cidServer.getGroupNo());
+//        if (serverId==null){
+//            return  R.error(501,"该地区服务器剩余数量不足");
+//        }
         account.setServerId(count.getId());
         account.setServerStatus(1);
         updateById(account);
 
-        cidIpadServerService.subtractServer(count.getId());
+//        cidIpadServerService.subtractServer(count.getId());
         CidIpadServerUser qwIpadServerUser = new CidIpadServerUser();
         qwIpadServerUser.setCompanyUserId(companyUser.getUserId());
         qwIpadServerUser.setCompanyId(companyUser.getCompanyId());
@@ -366,7 +364,6 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
             throw e;
         }
     }
-
     private void sendSyncContactCommand(String wxNo) {
         HttpUtil.get(wxApiUrl + "/app/common/syncWx?wxId=" + wxNo);
     }
@@ -595,9 +592,9 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
 //            // 老类型未找到,再查新类型
 //            if (waitingExec == null) {
             CompanyAiWorkflowExec  waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
-                        wxClientId,
-                        ExecutionStatusEnum.WAITING.getValue(),
-                        NodeTypeEnum.AI_ADD_WX_TASK_NEW.getValue());
+                    wxClientId,
+                    ExecutionStatusEnum.WAITING.getValue(),
+                    NodeTypeEnum.AI_ADD_WX_TASK_NEW.getValue());
 //                isNewNodeType = true;
 //            }
 
@@ -613,9 +610,9 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
             queryP.setStatus(ExecutionStatusEnum.WAITING.getValue());
             List<CompanyAiWorkflowExecLog> companyAiWorkflowExecLogs = companyAiWorkflowExecLogMapper.selectCompanyAiWorkflowExecLogList(queryP);
             companyAiWorkflowExecLogs.forEach(log -> {
-                log.setStatus(ExecutionStatusEnum.SUCCESS.getValue());
-                companyAiWorkflowExecLogMapper.updateById(log);
-                }
+                        log.setStatus(ExecutionStatusEnum.SUCCESS.getValue());
+                        companyAiWorkflowExecLogMapper.updateById(log);
+                    }
             );
 
             String workflowInstanceId = waitingExec.getWorkflowInstanceId();
@@ -627,7 +624,7 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
             // 互斥检查:根据节点类型使用对应的互斥方法
             boolean canExecute;
 //            if (isNewNodeType) {
-                canExecute = AiAddWxTaskNewNode.tryMarkAsExecuted(workflowInstanceId, wxClientId);
+            canExecute = AiAddWxTaskNewNode.tryMarkAsExecuted(workflowInstanceId, wxClientId);
 //            } else {
 //                canExecute = AiAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId);
 //            }

+ 5 - 2
fs-service/src/main/java/com/fs/company/service/impl/StatisticManageServiceImpl.java

@@ -8,7 +8,6 @@ import com.fs.company.domain.CourseWatchLogStatistics;
 import com.fs.company.mapper.CourseWatchLogStatisticsMapper;
 import com.fs.company.mapper.StatisticManageMapper;
 import com.fs.company.service.IStatisticManageService;
-import java.util.function.Function;
 import com.fs.statis.dto.ComprehensiveStatisticsDTO;
 import com.fs.statis.param.ComprehensiveStatisticsParam;
 import com.fs.utils.MyDateUtils;
@@ -22,7 +21,11 @@ import org.springframework.util.Assert;
 
 import javax.annotation.Resource;
 import java.math.BigDecimal;
-import java.util.*;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**

+ 87 - 2
fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java

@@ -9,6 +9,7 @@ import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.IWorkflowNode;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.ExecutionStatusEnum;
@@ -45,6 +46,7 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
     public static final WorkflowNodeFactory workflowNodeFactory = SpringUtils.getBean(WorkflowNodeFactory.class);
     public static final ObjectMapper objectMapper = new ObjectMapper();
     public static final RedissonClient redissonClient = SpringUtils.getBean(RedissonClient.class);
+    public static final ICompanyVoiceRoboticService companyVoiceRoboticService = SpringUtils.getBean(ICompanyVoiceRoboticService.class);
     protected static final String NODE_EXEC_LOCK_PREFIX = "node_exec_lock_";
     protected static final String CONTINUE_TIMER_EXECUTE_KEY = "CONTINUE:TIMER:EXECUTE:%s:%s:%s:";
     public static final String CONTINUE_TIMER_EXECUTE_KEY_PREFIX = "CONTINUE:TIMER:EXECUTE:%s:*";
@@ -108,13 +110,20 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
 
     @Override
     public ExecutionResult continueExecute(ExecutionContext context) {
+        // 暂停守卫:任务暂停时不处理继续请求
+        Long continueTaskId = getTaskIdFromContext(context);
+        if (isTaskPaused(continueTaskId)) {
+            log.info("任务已暂停,跳过continueExecute - taskId: {}, workflowInstanceId: {}, nodeKey: {}",
+                    continueTaskId, context.getWorkflowInstanceId(), nodeKey);
+            return null;
+        }
         if(!runnable(context) && getType() != NodeTypeEnum.END){
             log.info("当前流程已到达结束节点,节点继续执行失败:- {},- {} -,{}" , nodeName, nodeKey, context.getWorkflowInstanceId());
             return null;
         }
         // 校验节点执行是否符合时间设置
         CompanyAiWorkflowExec timeAvailable = companyAiWorkflowExecMapper.selectExecWithTimeAvailableByInstanceId(context.getWorkflowInstanceId());
-        
+
         if (timeAvailable == null) {
             CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());
             if (exec == null) {
@@ -131,7 +140,7 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             log.info("当前流程不在可执行时间范围内 continue已被阻塞等待唤醒执行,节点执行等待:- {},- {} -,{}" , nodeName, nodeKey, context.getWorkflowInstanceId());
             return null;
         }
-        
+
         RLock lock = null;
         try {
             String lockKey = NODE_EXEC_LOCK_PREFIX + context.getWorkflowInstanceId();
@@ -477,6 +486,23 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
         return context;
     }
 
+    /**
+     * 创建带businessId(roboticId)的执行上下文,用于延时任务中暂停守卫检查
+     */
+    protected ExecutionContext createExecutionContextWithBusinessId(String workflowInstanceId, String nodeKey) {
+        ExecutionContext context = createExecutionContext(workflowInstanceId, nodeKey);
+        try {
+            CompanyVoiceRoboticBusiness business = companyVoiceRoboticBusinessMapper
+                    .selectCompanyVoiceRoboticBusinessByWorkflowInstanceId(workflowInstanceId);
+            if (business != null) {
+                context.setBusinessId(business.getRoboticId());
+            }
+        } catch (Exception e) {
+            log.warn("createExecutionContextWithBusinessId获取roboticId失败 - workflowInstanceId: {}", workflowInstanceId, e);
+        }
+        return context;
+    }
+
     protected void execPointNextNode(ExecutionContext context) {
         try {
             CompanyAiWorkflowExec update = new CompanyAiWorkflowExec();
@@ -499,6 +525,14 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
         if (StringUtils.isBlank(edge.getTargetNodeKey())) {
             return;
         }
+        // 暂停守卫:任务暂停时不触发下一节点执行,将实例状态设为READY并指向下一节点
+        Long nextTaskId = getTaskIdFromContext(context);
+        if (isTaskPaused(nextTaskId)) {
+            log.info("任务已暂停,runNextNode中止执行 - taskId: {}, workflowInstanceId: {}, nextNodeKey: {}",
+                    nextTaskId, context.getWorkflowInstanceId(), edge.getTargetNodeKey());
+            updateExecToReady(context, edge.getTargetNodeKey());
+            return;
+        }
         ExecutionContext nextContext = context.clone();
         CompanyWorkflowNode nextNode = getNodeByKey(edge.getTargetNodeKey());
         nextContext.setCurrentNodeKey(nextNode.getNodeKey());
@@ -550,4 +584,55 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
     public static String getContinueTimerExecuteKey(Integer groupNo, LocalTime time) {
         return String.format(CONTINUE_TIMER_EXECUTE_KEY, groupNo,time.getHour(), time.getMinute());
     }
+
+    /**
+     * 从执行上下文中获取任务ID(roboticId)
+     * 通过workflowInstanceId查询业务关联表获取
+     */
+    protected Long getTaskIdFromContext(ExecutionContext context) {
+        try {
+            CompanyVoiceRoboticBusiness business = companyVoiceRoboticBusinessMapper
+                    .selectCompanyVoiceRoboticBusinessByWorkflowInstanceId(context.getWorkflowInstanceId());
+            if (business != null) {
+                return business.getRoboticId();
+            }
+        } catch (Exception e) {
+            log.warn("获取taskId失败 - workflowInstanceId: {}", context.getWorkflowInstanceId(), e);
+        }
+        return null;
+    }
+
+    /**
+     * 判断任务是否处于暂停状态
+     */
+    protected boolean isTaskPaused(Long taskId) {
+        if (taskId == null) {
+            return false;
+        }
+        return companyVoiceRoboticService.isTaskPaused(taskId);
+    }
+
+    /**
+     * 将工作流实例状态设为READY并指向下一节点(暂停守卫使用)
+     * 恢复后CidTask定时扫描会自然拾取READY状态的实例继续执行
+     */
+    protected void updateExecToReady(ExecutionContext context, String nextNodeKey) {
+        try {
+            CompanyWorkflowNode nextNode = getNodeByKey(nextNodeKey);
+            CompanyAiWorkflowExec update = new CompanyAiWorkflowExec();
+            update.setWorkflowInstanceId(context.getWorkflowInstanceId());
+            update.setStatus(ExecutionStatusEnum.READY.getValue());
+            update.setCurrentNodeKey(nextNodeKey);
+            update.setCurrentNodeName(nextNode.getNodeName());
+            update.setCurrentNodeType(NodeTypeEnum.fromCode(nextNode.getNodeType()).getValue());
+            update.setLastUpdateTime(LocalDateTime.now());
+            update.setVariables(objectMapper.writeValueAsString(context.getVariables()));
+            companyAiWorkflowExecMapper.updateByWorkflowInstanceId(update);
+            log.info("任务暂停,工作流实例已设为READY状态 - workflowInstanceId: {}, nextNodeKey: {}",
+                    context.getWorkflowInstanceId(), nextNodeKey);
+        } catch (Exception e) {
+            log.error("更新工作流为READY状态失败 - workflowInstanceId: {}, nextNodeKey: {}",
+                    context.getWorkflowInstanceId(), nextNodeKey, e);
+        }
+    }
 }

+ 49 - 8
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java

@@ -1,14 +1,16 @@
 package com.fs.company.service.impl.call.node;
 
-import cn.hutool.core.util.RandomUtil;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
-import com.fs.company.mapper.*;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.mapper.CompanyWxClientMapper;
+import com.fs.company.mapper.CompanyWxDialogMapper;
 import com.fs.company.param.AddWxActionParam;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.util.ObjectPlaceholderResolver;
@@ -16,6 +18,7 @@ import com.fs.company.vo.AiAddWxConfigVO;
 import com.fs.company.vo.AiCallWorkflowConditionVo;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
@@ -47,6 +50,7 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
     public static final String DELAY_ADD_WX_NEW_KEY = "addWxTaskNew:delay:%s:%s:%s:";
     private static final CompanyWxDialogMapper companyWxDialogMapper = SpringUtils.getBean(CompanyWxDialogMapper.class);
     private static final CrmCustomerServiceImpl crmCustomerService = SpringUtils.getBean(CrmCustomerServiceImpl.class);
+    private static final CrmCustomerMapper crmCustomerMapper = SpringUtils.getBean(CrmCustomerMapper.class);
     private static final ObjectPlaceholderResolver objectPlaceholderResolver = SpringUtils.getBean(ObjectPlaceholderResolver.class);
     private static final ISysConfigService sysConfigService = SpringUtils.getBean(ISysConfigService.class);
     private static final CidIpadServerMapper cidIpadServerMapper = SpringUtils.getBean(CidIpadServerMapper.class);
@@ -95,10 +99,25 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
             }
 
             WxContact wxQuery = companyAiWorkflowExecMapper.selectWxContectByWorkflowInstanceId(context.getWorkflowInstanceId());
-            wxQuery.setRemark(wxQuery.getRemark() + RandomUtil.randomNumbers(10));
-            wxQuery.setNickName(wxQuery.getRemark());
-            wxQuery.setFriends(0);
-            wxContactMapper.insert(wxQuery);
+            Long crmUserId = wxQuery.getCrmUserId();
+            WxContact wxContact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("phone", wxQuery.getPhone()).eq("account_id", wxQuery.getAccountId()));
+            if(wxContact != null){
+                wxQuery = wxContact;
+                wxQuery.setRemark(wxQuery.getRemark());
+                wxQuery.setNickName(wxQuery.getRemark());
+                wxQuery.setFriends(0);
+                wxQuery.setCustomerId(crmUserId);
+                wxContactMapper.updateById(wxQuery);
+            }else{
+                wxQuery.setRemark(wxQuery.getRemark());
+                wxQuery.setNickName(wxQuery.getRemark());
+                wxQuery.setFriends(0);
+                wxQuery.setCrmUserId(crmUserId);
+                wxContactMapper.insert(wxQuery);
+            }
+            CrmCustomer crmCustomer = crmCustomerMapper.selectById(crmUserId);
+            crmCustomer.setWxContactId(wxQuery.getId());
+            crmCustomerService.updateCrmCustomer(crmCustomer);
 
             CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
             CompanyWxClient update = new CompanyWxClient();
@@ -109,7 +128,8 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
             pendingAddWx(wxQuery.getAccountId(), wxQuery.getRemark(),
                     wxQuery.getPhone(),
                     addWxConfig.getDialogId(),
-                    wxQuery.getCrmUserId(),
+                    crmUserId,
+                    wxQuery.getId(),
                     context.getWorkflowInstanceId(),
                     context.getCurrentNodeKey());
             return ExecutionResult.paused()
@@ -185,6 +205,24 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
             log.error("当前节点已流转 ,目标:{},实际:{}", nodeKey, exec.getCurrentNodeKey());
             return;
         }
+        // 从exec中恢复关键变量,确保延时Redis中的context包含暂停守卫所需的roboticId等信息
+        if (exec.getVariables() != null) {
+            Map<String, Object> savedVariables = JSONObject.parseObject(exec.getVariables(), Map.class);
+            if (savedVariables != null) {
+                Object roboticId = savedVariables.get("roboticId");
+                if (roboticId != null) {
+                    context.setVariable("roboticId", roboticId);
+                }
+                Object businessId = savedVariables.get("businessId");
+                if (businessId != null) {
+                    context.setVariable("businessId", businessId);
+                }
+                Object cidGroupNo = savedVariables.get("cidGroupNo");
+                if (cidGroupNo != null) {
+                    context.setVariable("cidGroupNo", cidGroupNo);
+                }
+            }
+        }
         // 更新加微日志执行状态
         super.updateLogStatusIfExist(context, ExecutionStatusEnum.PAUSED, ExecutionStatusEnum.WAITING);
         super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), nodeKey, context, ExecutionStatusEnum.WAITING);
@@ -276,7 +314,7 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
      * @param instanceId
      * @param nodeKey
      */
-    private void pendingAddWx(Long accountId, String remark, String phone, Long dialogId, Long crmUserId,String instanceId,String nodeKey) {
+    private void pendingAddWx(Long accountId, String remark, String phone, Long dialogId, Long crmUserId, Long wxContactId,String instanceId,String nodeKey) {
         try {
             // 1. 获取基础数据
             CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(accountId);
@@ -304,6 +342,9 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
             JSONObject bizJson = new JSONObject()
                     .fluentPut("instanceId",instanceId)
                     .fluentPut("nodeKey",nodeKey)
+                    .fluentPut("accountId",companyWxAccount.getId())
+                    .fluentPut("wxContactId",wxContactId)
+                    .fluentPut("crmUserId",crmUserId)
                     .fluentPut("accountId",companyWxAccount.getId());
             param.setBizJson(bizJson.toJSONString());
             wxService.addWx(param);

+ 7 - 7
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNode.java

@@ -1,6 +1,5 @@
 package com.fs.company.service.impl.call.node;
 
-import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.redis.RedisCacheT;
@@ -8,19 +7,20 @@ import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
-import com.fs.company.mapper.CompanyWxClientMapper;
 import com.fs.company.mapper.CompanyWorkflowNodeMapper;
+import com.fs.company.mapper.CompanyWxClientMapper;
 import com.fs.company.param.ExecutionContext;
-import com.fs.company.service.IWorkflowNode;
-import com.fs.company.vo.*;
+import com.fs.company.vo.AiAddWxConfigVO;
+import com.fs.company.vo.AiCallWorkflowConditionVo;
+import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.Date;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
@@ -257,7 +257,7 @@ public class AiAddWxTaskNode extends AbstractWorkflowNode {
      * @param workflowInstanceId
      */
     public void doneAddwx(String workflowInstanceId) {
-        ExecutionContext context = createExecutionContext(workflowInstanceId, nodeKey);
+        ExecutionContext context = createExecutionContextWithBusinessId(workflowInstanceId, nodeKey);
         context.setVariable("lastNodeKey", nodeKey);
         //启动定时节点倒计时
         CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());
@@ -290,7 +290,7 @@ public class AiAddWxTaskNode extends AbstractWorkflowNode {
                     String redisKey = getDelayAddWxKeyPrefix(exec.getCidGroupNo(),l) + workflowInstanceId;
                     ExecutionContext nextContext = context.clone();
                     nextContext.setCurrentNodeKey(edge.getTargetNodeKey());
-                    super.redisCache.setCacheObject(redisKey, nextContext);
+                    super.redisCache.setCacheObject(redisKey, nextContext, 1, TimeUnit.DAYS);
                 }
             }
         });

+ 52 - 16
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -19,15 +19,13 @@ import com.fs.company.vo.AiCallWorkflowConditionVo;
 import com.fs.company.vo.CompanyVoiceRoboticCallBlacklistCheckVO;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.company.vo.easycall.EasyCallCommonAddCallListParam;
-import com.fs.company.vo.easycall.EasyCallCreateTaskParam;
 import com.fs.company.vo.easycall.EasyCallPhoneItemVO;
-import com.fs.company.vo.easycall.EasyCallTaskVO;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
-import com.fs.enums.TaskTypeEnum;
 import com.fs.his.config.CidPhoneConfig;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
@@ -116,8 +114,8 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
                 //拨通
                 if (condition.isCallConnected() && callRes.getCallTime() != null && callRes.getCallTime() > 0) {
                     //如果含有意向度过滤
-                    if (StringUtils.isNotBlank(condition.getIntention())) {
-                        if (condition.getIntention().equals(callRes.getIntention())) {
+                    if (condition.getIntention() != null && !condition.getIntention().isEmpty()) {
+                        if (condition.getIntention().contains(callRes.getIntention())) {
                             runNextNode(context, edge);
                             runnableCount++;
                             break;
@@ -134,6 +132,15 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
                 }
                 //未拨通
                 else if (!condition.isCallConnected() && (callRes.getCallTime() == null || Long.valueOf(0).equals(callRes.getCallTime()) || callRes.getCallAnswerTime() == null)) {
+                    //如果含有未拨通类型过滤
+                    if (condition.getHangupType() != null && !condition.getHangupType().isEmpty()) {
+                        if (StringUtils.isNotBlank(callRes.getHangupType()) && condition.getHangupType().contains(callRes.getHangupType())) {
+                            //匹配到未拨通类型,继续执行
+                        } else {
+                            log.info("流程:{},节点:{},未拨通类型设置:{},实际未拨通类型:{}, 未拨通类型不符设置中断执行,", context.getWorkflowInstanceId(), nodeKey, condition.getHangupType(), callRes.getHangupType());
+                            continue;
+                        }
+                    }
                     //延时操作
                     if (null != condition.getCallTime() && condition.getCallTime() > 0) {
                         //计算延时分片分钟
@@ -207,7 +214,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
                 }
 //                companyVoiceRoboticService.workflowCallPhoneOne(bus.getRoboticId(), bus.getCalleeId(), context, callConfigVo);
                 // EasyCallCenter365 外呼
-                 workflowCallPhoneOne4EasyCall(bus.getRoboticId(),bus.getCalleeId(), context, callConfigVo);
+                workflowCallPhoneOne4EasyCall(bus.getRoboticId(),bus.getCalleeId(), context, callConfigVo);
                 super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.PAUSED);
                 return ExecutionResult.paused()
                         .outputData(context.getVariables())
@@ -272,7 +279,6 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
             if(null != cidConf && !StringUtil.strIsNullOrEmpty(cidConf.getCallbackUrl())){
                 callBackUrl = cidConf.getCallbackUrl();
             }
-
             //读取总后台配置
             if(StringUtil.strIsNullOrEmpty(callBackUrl)){
                 String s = configService.selectConfigByKey("cid.config");
@@ -292,8 +298,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         callbackInfo.put("nodeKey", context.getCurrentNodeKey());
         callbackInfo.put("workflowInstanceId", context.getWorkflowInstanceId());
         callbackInfo.put("calleeId", calleeId);
-        super.redisCache.setCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + callBackUuid,
-                callbackInfo.toJSONString(), 1, TimeUnit.DAYS);
+        super.redisCache.setCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + callBackUuid,callbackInfo.toJSONString());
         // 将 callBackUuid 写入 context,供后续回调时从 context 取用
         context.setVariable("callBackUuid", callBackUuid);
 
@@ -362,13 +367,24 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         addListParam.setBatchId(batchId);
         // 构建号码条目,bizJson 传入默认客户信息占位符
         EasyCallPhoneItemVO phoneItem = new EasyCallPhoneItemVO();
-        phoneItem.setPhoneNum(callees.getPhone());
+        // todo
+        phoneItem.setPhoneNum(PhoneUtil.decryptPhone(callees.getPhone()));
+
+
+//        phoneItem.setPhoneNum(callees.getPhone());
         // bizJson 默认传入客户姓名占位,运行时可根据实际业务填充
         phoneItem.setBizJson(new JSONObject().fluentPut("custName", callees.getUserName()).fluentPut("callBackUuid",callBackUuid).fluentPut("callBackUrl",callBackUrl));
         addListParam.setPhoneList(Collections.singletonList(phoneItem));
-        easyCallService.addCommonCallList(addListParam, null);
-        log.info("workflowCallPhoneOne4EasyCall: 名单追加成功 - batchId: {}, phone: {}",
-                batchId, callees.getPhone());
+        Long gatewayId = callConfigVo.getGatewayId();
+
+        boolean b = easyCallService.addCommonCallList(addListParam, robotic.getCompanyId(), gatewayId);
+        if(b){
+            log.info("workflowCallPhoneOne4EasyCall: 名单追加成功 - batchId: {}, phone: {}",
+                    batchId, callees.getPhone());
+        }else{
+            log.error("workflowCallPhoneOne4EasyCall: 名单追失败 - batchId: {}, phone: {}",
+                    batchId, callees.getPhone());
+        }
 
         // 6. 启动外呼任务
         easyCallService.startTask(batchId, null);
@@ -416,9 +432,29 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         if(null != sipTaskInfo && null != sipTaskInfo.getBatchId()){
             return  sipTaskInfo.getBatchId();
         }
-        //没有的情况下创建任务并返回
-        CompanyAiWorkflowExec companyAiWorkflowExec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
-        return companyWorkflowEngine.createSipTask(roboticId, companyAiWorkflowExec.getWorkflowId());
+        String lockKey = "sipTask:lock:" + roboticId + ":" + nodeKey;
+        boolean locked = super.redisCache.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
+        if (!locked) {
+            log.info("getTaskBatchId: 其他线程正在创建SIP任务,等待后重试 - roboticId: {}, nodeKey: {}", roboticId, nodeKey);
+            for (int i = 0; i < 20; i++) {
+                try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
+                sipTaskInfo = companySiptaskInfoMapper.selectSipTaskInfoByTaskIdAndNodeKey(roboticId, nodeKey);
+                if (null != sipTaskInfo && null != sipTaskInfo.getBatchId()) {
+                    return sipTaskInfo.getBatchId();
+                }
+            }
+            log.warn("getTaskBatchId: 等待超时,尝试直接创建 - roboticId: {}, nodeKey: {}", roboticId, nodeKey);
+        }
+        try {
+            sipTaskInfo = companySiptaskInfoMapper.selectSipTaskInfoByTaskIdAndNodeKey(roboticId, nodeKey);
+            if (null != sipTaskInfo && null != sipTaskInfo.getBatchId()) {
+                return sipTaskInfo.getBatchId();
+            }
+            CompanyAiWorkflowExec companyAiWorkflowExec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
+            return companyWorkflowEngine.createSipTask(roboticId, companyAiWorkflowExec.getWorkflowId());
+        } finally {
+            super.redisCache.deleteObject(lockKey);
+        }
     }
 //    @Override
 //    protected void postExecute(ExecutionContext context, ExecutionResult result) {

+ 6 - 3
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java

@@ -6,7 +6,10 @@ import com.fs.common.constant.Constants;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
-import com.fs.company.domain.*;
+import com.fs.company.domain.CompanyAiWorkflowExec;
+import com.fs.company.domain.CompanyVoiceRoboticBusiness;
+import com.fs.company.domain.CompanyWorkflowEdge;
+import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.mapper.CompanyWxClientMapper;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.vo.AiCallWorkflowConditionVo;
@@ -31,7 +34,7 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
     private static final CompanyWxClientMapper companyWxClientMapper = SpringUtils.getBean(CompanyWxClientMapper.class);
     @SuppressWarnings("unchecked")
     private static final RedisCacheT<String> redisCache = SpringUtils.getBean(RedisCacheT.class);
-    public static final String DELAY_QW_ADD_WX_KEY = "qwAddWxTask:delay:%s:%s:%s:";
+    public static final String DELAY_QW_ADD_WX_KEY = "qwAddWxTask:delay:%s:%s:%s";
     /**
      * 默认加微超时时间(分钟)
      */
@@ -205,7 +208,7 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
      *
      */
     public void doneQwAddWx(String workflowInstanceId) {
-        ExecutionContext context = createExecutionContext(workflowInstanceId, nodeKey);
+        ExecutionContext context = createExecutionContextWithBusinessId(workflowInstanceId, nodeKey);
         context.setVariable("lastNodeKey", nodeKey);
         //启动定时节点倒计时
         CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());

+ 0 - 1
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiSendMsgTaskNode.java

@@ -10,7 +10,6 @@ import com.fs.company.domain.CompanyWorkflowNode;
 import com.fs.company.mapper.CompanyWorkflowNodeMapper;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.ICompanyVoiceRoboticService;
-import com.fs.company.service.IWorkflowNode;
 import com.fs.company.vo.AiCallWorkflowConditionVo;
 import com.fs.company.vo.AiSendMsgConfigVO;
 import com.fs.company.vo.ExecutionResult;

+ 52 - 0
fs-service/src/main/java/com/fs/company/service/impl/call/node/EasyCallTaskControlService.java

@@ -0,0 +1,52 @@
+package com.fs.company.service.impl.call.node;
+
+import com.fs.company.service.easycall.IEasyCallService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * EasyCallCenter365 任务暂停/恢复控制服务
+ * <p>
+ * 通过HTTP接口调用EasyCall平台,实现对第三方外呼任务的暂停和恢复操作。
+ * 当前为Mock实现,待提供真实接口地址后替换。
+ */
+@Service
+@Slf4j
+public class EasyCallTaskControlService {
+
+    @Value("${easycall.api.base-url:http://localhost:8870}")
+    private String easyCallBaseUrl;
+
+    @Autowired
+    private IEasyCallService easyCallService;
+
+    /**
+     * 暂停EasyCall外呼任务
+     *
+     * @param batchId EasyCall任务批次ID
+     */
+    public void pauseTask(Long batchId) {
+        try {
+            easyCallService.stopTask(batchId, null);
+            log.info("成功暂停EasyCall任务: batchId={}", batchId);
+        } catch (Exception e) {
+            log.error("暂停EasyCall任务失败: batchId={}", batchId, e);
+        }
+    }
+
+    /**
+     * 恢复EasyCall外呼任务
+     *
+     * @param batchId EasyCall任务批次ID
+     */
+    public void resumeTask(Long batchId) {
+        try {
+            easyCallService.startTask(batchId, null);
+            log.info("成功恢复EasyCall任务: batchId={}", batchId);
+        } catch (Exception e) {
+            log.error("恢复EasyCall任务失败: batchId={}", batchId, e);
+        }
+    }
+}

+ 3 - 7
fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java

@@ -1,15 +1,11 @@
 package com.fs.company.service.impl.call.node;
 
 import com.fs.common.utils.spring.SpringUtils;
-import com.fs.company.domain.CompanyAiWorkflowExecLog;
-import com.fs.company.domain.CompanyVoiceRobotic;
-import com.fs.company.domain.CompanyVoiceRoboticBusiness;
 import com.fs.company.mapper.CompanyWorkflowNodeMapper;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
-import com.fs.enums.TaskTypeEnum;
 
 import java.util.Map;
 
@@ -44,9 +40,9 @@ public class EndNode extends AbstractWorkflowNode {
      */
     @Override
     protected void postExecute(ExecutionContext context, ExecutionResult result) {
-      super.postExecute(context, result);
-      super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.SUCCESS);
-      super.taskFinish(context);
+        super.postExecute(context, result);
+        super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.SUCCESS);
+        super.taskFinish(context);
 //      //判定是否任务完成了更新任务的状态为执行完成
 //        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
 //        if(null != roboticBusiness){

+ 1 - 6
fs-service/src/main/java/com/fs/company/service/impl/call/node/StartNode.java

@@ -1,21 +1,16 @@
 package com.fs.company.service.impl.call.node;
 
-import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.CompanyAiWorkflowExec;
-import com.fs.company.domain.CompanyAiWorkflowExecLog;
 import com.fs.company.domain.CompanyWorkflowEdge;
-import com.fs.common.utils.spring.SpringUtils;
-import com.fs.company.domain.CompanyWorkflowNode;
 import com.fs.company.mapper.CompanyWorkflowNodeMapper;
 import com.fs.company.param.ExecutionContext;
-import com.fs.company.service.IWorkflowNode;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 
 import java.util.List;
 import java.util.Map;
-import java.util.Random;
 
 /**
  * @author MixLiu

+ 3 - 4
fs-service/src/main/java/com/fs/company/service/impl/call/node/WorkflowNodeFactory.java

@@ -3,7 +3,6 @@ package com.fs.company.service.impl.call.node;
 import com.fs.company.service.IWorkflowNode;
 import com.fs.company.service.IWorkflowNodeFactory;
 import com.fs.enums.NodeTypeEnum;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
 import java.util.Map;
@@ -36,12 +35,12 @@ public class WorkflowNodeFactory implements IWorkflowNodeFactory {
             case AI_SEND_MSG_TASK:
                 node = new AiSendMsgTaskNode(nodeKey, nodeName, properties);
                 break;
-            case AI_ADD_WX_TASK:
-                node = new AiAddWxTaskNode(nodeKey, nodeName, properties);
-                break;
             case AI_ADD_WX_TASK_NEW:
                 node = new AiAddWxTaskNewNode(nodeKey, nodeName, properties);
                 break;
+            case AI_ADD_WX_TASK:
+                node = new AiAddWxTaskNode(nodeKey, nodeName, properties);
+                break;
             case AI_QW_ADD_WX_TASK:
                 node = new AiQwAddWxTaskNode(nodeKey, nodeName, properties);
                 break;

+ 9 - 2
fs-service/src/main/java/com/fs/company/vo/AiCallWorkflowConditionVo.java

@@ -2,14 +2,21 @@ package com.fs.company.vo;
 
 import lombok.Data;
 
+import java.util.List;
+
 @Data
 public class AiCallWorkflowConditionVo {
     // 外呼配置-是否拨通
     private boolean callConnected;
     // 外呼配置-延迟时间
     private Integer callTime;
-    // 外呼配置-意向度
-    private String intention;
+//    // 外呼配置-意向度
+//    private String intention;
+
+    // 外呼配置-意向度(多选)
+    private List<String> intention;
+    // 外呼配置-未拨通类型(多选)
+    private List<String> hangupType;
 
 
     // 加微条件-是否同意

+ 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 - 5
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallBlacklistCheckVO.java

@@ -52,10 +52,10 @@ public class CompanyVoiceRoboticCallBlacklistCheckVO {
     }
 
     public static CompanyVoiceRoboticCallBlacklistCheckVO reject(Long blacklistId,
-                                                   String businessType,
-                                                   Integer targetType,
-                                                   String targetValue,
-                                                   String reason) {
+                                                                 String businessType,
+                                                                 Integer targetType,
+                                                                 String targetValue,
+                                                                 String reason) {
         CompanyVoiceRoboticCallBlacklistCheckVO vo = new CompanyVoiceRoboticCallBlacklistCheckVO();
         vo.setPass(false);
         vo.setHitBlacklist(true);
@@ -75,4 +75,4 @@ public class CompanyVoiceRoboticCallBlacklistCheckVO {
         return vo;
     }
 
-}
+}

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

@@ -1,8 +1,5 @@
 package com.fs.company.vo;
 
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.baomidou.mybatisplus.annotation.TableId;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
@@ -44,6 +41,9 @@ public class CompanyVoiceRoboticCallLogAddwxVO {
     /** 个微账号id */
     @Excel(name = "个微账号id")
     private Long wxAccountId;
+    /**
+    * 客户昵称
+    */
     private String wxAccountName;
 
     private Long customerId;

+ 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;
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio