吴树波 hai 3 semanas
pai
achega
ef966b5252
Modificáronse 21 ficheiros con 930 adicións e 48 borrados
  1. 14 0
      .vscode/launch.json
  2. 3 0
      .vscode/settings.json
  3. 13 0
      fs-common/src/main/java/com/fs/common/constant/Constants.java
  4. 14 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  5. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyAiWorkflowExec.java
  6. 8 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecLogMapper.java
  7. 21 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java
  8. 8 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  9. 162 12
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  10. 8 3
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  11. 60 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java
  12. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java
  13. 220 20
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNode.java
  14. 33 0
      fs-service/src/main/java/com/fs/company/vo/AiAddWxWorkflowConditionVo.java
  15. 189 0
      fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java
  16. 1 5
      fs-service/src/main/java/com/fs/enums/ExecutionStatusEnum.java
  17. 7 0
      fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecLogMapper.xml
  18. 45 6
      fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecMapper.xml
  19. 4 1
      fs-service/src/main/resources/mapper/company/CompanyWorkflowMapper.xml
  20. 107 0
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java
  21. 9 0
      fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

+ 14 - 0
.vscode/launch.json

@@ -0,0 +1,14 @@
+{
+    "configurations": [
+        {
+            "type": "java",
+            "name": "Spring Boot-FsCompanyApplication<fs-company>",
+            "request": "launch",
+            "cwd": "${workspaceFolder}",
+            "mainClass": "com.fs.FsCompanyApplication",
+            "projectName": "fs-company",
+            "args": "",
+            "envFile": "${workspaceFolder}/.env"
+        }
+    ]
+}

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "java.compile.nullAnalysis.mode": "automatic"
+}

+ 13 - 0
fs-common/src/main/java/com/fs/common/constant/Constants.java

@@ -192,4 +192,17 @@ public class Constants
      * CID 执行下一个 roboticID + calleesId
      */
     public static final String CID_NEXT_TASK_ID = "cid:next:task:";
+
+    /**
+     * 工作流加微超时检测 Key: workflow:addwx:timeout:{workflowInstanceId}:{wxClientId}
+     * Value: 超时时间戳
+     */
+    public static final String WORKFLOW_ADD_WX_TIMEOUT = "workflow:addwx:timeout:";
+
+    /**
+     * 工作流加微执行状态标记 Key: workflow:addwx:executed:{workflowInstanceId}:{wxClientId}
+     * Value: 1-已执行
+     * 用于实现回调成功和超时互斥,只执行一个
+     */
+    public static final String WORKFLOW_ADD_WX_EXECUTED = "workflow:addwx:executed:";
 }

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

@@ -23,6 +23,7 @@ import com.fs.company.domain.CompanyVoiceRoboticWx;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.vo.WorkflowExecRecordVo;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -239,4 +240,17 @@ public class CompanyVoiceRoboticController extends BaseController
         List<CIDGroupListResult> cidGroupList = aiCallService.getCIDGroupList(null, loginUser.getCompany().getCompanyId());
         return R.ok().put("data", cidGroupList);
     }
+
+    /**
+     * 查询任务执行记录
+     * 获取每个人的工作流执行状态和节点日志
+     */
+    @GetMapping("/execRecords")
+    public R getExecRecords(Long roboticId) {
+        if (roboticId == null) {
+            return R.error("任务ID不能为空");
+        }
+        List<WorkflowExecRecordVo> records = companyVoiceRoboticService.getExecRecords(roboticId);
+        return R.ok().put("data", records);
+    }
 }

+ 3 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyAiWorkflowExec.java

@@ -2,6 +2,8 @@ package com.fs.company.domain;
 
 import java.time.LocalDateTime;
 import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.IdType;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
@@ -19,6 +21,7 @@ import lombok.EqualsAndHashCode;
 public class CompanyAiWorkflowExec {
 
     /** $column.columnComment */
+    @TableId(type = IdType.AUTO)
     private Long id;
 
     /** 工作流执行实例id */

+ 8 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecLogMapper.java

@@ -3,6 +3,7 @@ package com.fs.company.mapper;
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyAiWorkflowExecLog;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * AI外呼流程执行记录Mapper接口
@@ -58,4 +59,11 @@ public interface CompanyAiWorkflowExecLogMapper extends BaseMapper<CompanyAiWork
      * @return 结果
      */
     int deleteCompanyAiWorkflowExecLogByIds(Long[] ids);
+
+    /**
+     * 根据工作流实例ID查询执行日志
+     * @param workflowInstanceId 工作流实例ID
+     * @return 执行日志列表
+     */
+    List<CompanyAiWorkflowExecLog> selectByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
 }

+ 21 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java

@@ -3,8 +3,11 @@ package com.fs.company.mapper;
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyAiWorkflowExec;
+import com.fs.company.vo.WorkflowExecRecordVo;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.List;
+
 /**
  * AI外呼工作流执行Mapper接口
  * 
@@ -70,4 +73,22 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
     CompanyAiWorkflowExec selectByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
 
     CompanyAiWorkflowExec selectByWorkflowInstanceIdAndCurrentNode(@Param("workflowInstanceId") String workflowInstanceId, @Param("currentNode") String currentNode);
+
+    /**
+     * 根据wxClientId查找等待中的加微工作流实例
+     * @param wxClientId 加微客户ID
+     * @param status 工作流状态
+     * @param nodeType 节点类型
+     * @return 工作流实例
+     */
+    CompanyAiWorkflowExec selectWaitingAddWxWorkflowByWxClientId(@Param("wxClientId") Long wxClientId,
+                                                                   @Param("status") Integer status,
+                                                                   @Param("nodeType") Integer nodeType);
+
+    /**
+     * 根据任务ID查询执行记录列表
+     * @param roboticId 任务ID
+     * @return 执行记录列表
+     */
+    List<WorkflowExecRecordVo> selectExecRecordsByRoboticId(@Param("roboticId") Long roboticId);
 }

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

@@ -8,6 +8,7 @@ import com.fs.company.param.ExecutionContext;
 import com.fs.company.vo.AddWxClientVo;
 import com.fs.company.vo.AiCallConfigVO;
 import com.fs.company.vo.CompanyVoiceRoboticQwUserListVo;
+import com.fs.company.vo.WorkflowExecRecordVo;
 
 import java.util.List;
 import java.util.Set;
@@ -85,5 +86,12 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
 
     void taskRun(Long id);
 
+    /**
+     * 查询任务执行记录列表
+     * @param roboticId 任务ID
+     * @return 执行记录列表
+     */
+    List<WorkflowExecRecordVo> getExecRecords(Long roboticId);
+
     void finishAddWxByCallees(Set<Long> roboticIds);
 }

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

@@ -37,6 +37,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
@@ -95,6 +97,8 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     private final CompanyVoiceRoboticBusinessMapper companyVoiceRoboticBusinessMapper;
     private final CompanyWorkflowEngine companyWorkflowEngine;
+    private final CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
+    private final CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
 
     @Autowired
     @Qualifier("workFlowExecutor")
@@ -806,28 +810,86 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     }
 
     @Override
-    @Transactional
     public void taskRun(Long id) {
         CompanyVoiceRobotic robotic = getById(id);
+        if (robotic == null) {
+            throw new RuntimeException("任务不存在: " + id);
+        }
+        if (robotic.getCompanyAiWorkflowId() == null) {
+            throw new RuntimeException("任务未配置工作流: " + id);
+        }
+
         robotic.setTaskStatus(1);
         updateById(robotic);
-        //根据任务加微方式决定是否直接分配微信 平均时 直接分配用户
-        if(Integer.valueOf(0).equals(robotic.getAddType())){
+
+        // 根据任务加微方式决定是否直接分配微信 平均时 直接分配用户
+        if (Integer.valueOf(0).equals(robotic.getAddType())) {
             allocateWx(robotic);
         }
-        //新增启动写入任务业务表数据
+
+        // 新增启动写入任务业务表数据
         buildTaskBussiness(robotic);
-        List<CompanyVoiceRoboticBusiness> roboticBusinesseList = companyVoiceRoboticBusinessMapper.selectList(new QueryWrapper<CompanyVoiceRoboticBusiness>().eq("robotic_id", id));
-        roboticBusinesseList.forEach(e -> {
-            Map<String, Object> map = new HashMap<>();
-            map.put("roboticId", robotic.getId());
-            map.put("businesse", e.getId());
-            // 写入执行任务
-            ExecutionResult initialize = companyWorkflowEngine.initialize(robotic.getCompanyAiWorkflowId(), map);
-            companyWorkflowEngine.executeNode(initialize.getWorkflowInstanceId(), initialize.getNextNodeKey());
 
+        // 查询业务列表
+        List<CompanyVoiceRoboticBusiness> roboticBusinesseList = companyVoiceRoboticBusinessMapper
+                .selectList(new QueryWrapper<CompanyVoiceRoboticBusiness>().eq("robotic_id", id));
+
+        if (roboticBusinesseList.isEmpty()) {
+            log.warn("任务没有业务数据: {}", id);
+            return;
+        }
+
+        // 初始化并执行工作流
+        initAndExecuteWorkflows(robotic, roboticBusinesseList);
+    }
+
+    /**
+     * 初始化并执行工作流
+     */
+    private void initAndExecuteWorkflows(CompanyVoiceRobotic robotic, List<CompanyVoiceRoboticBusiness> roboticBusinesseList) {
+        Long id = robotic.getId();
+        log.info("开始初始化工作流 - roboticId: {}, 业务数量: {}", id, roboticBusinesseList.size());
+
+        final Long workflowId = robotic.getCompanyAiWorkflowId();
+        final Long roboticId = robotic.getId();
+
+        // 先初始化所有工作流实例
+        List<ExecutionResult> initResults = new ArrayList<>();
+        for (CompanyVoiceRoboticBusiness business : roboticBusinesseList) {
+            try {
+                Map<String, Object> inputVariables = new HashMap<>();
+                inputVariables.put("roboticId", roboticId);
+                inputVariables.put("businessId", business.getId());
+
+                ExecutionResult initialize = companyWorkflowEngine.initialize(workflowId, inputVariables);
+
+                if (!initialize.isSuccess()) {
+                    log.error("工作流初始化失败 - businessId: {}, error: {}",
+                            business.getId(), initialize.getErrorMessage());
+                    continue;
+                }
+
+                log.info("工作流初始化成功 - businessId: {}, workflowInstanceId: {}",
+                        business.getId(), initialize.getWorkflowInstanceId());
+
+                initResults.add(initialize);
+            } catch (Exception ex) {
+                log.error("工作流初始化异常 - businessId: {}", business.getId(), ex);
+            }
+        }
+
+        log.info("工作流初始化完成 - roboticId: {}, 成功数: {}/{}", id, initResults.size(), roboticBusinesseList.size());
+
+        // 再并行执行所有工作流
+        initResults.forEach(result -> {
+            try {
+                companyWorkflowEngine.executeNode(result.getWorkflowInstanceId(), result.getNextNodeKey());
+            } catch (Exception ex) {
+                log.error("工作流执行异常 - workflowInstanceId: {}", result.getWorkflowInstanceId(), ex);
+            }
         });
 
+        log.info("工作流启动完成 - roboticId: {}", id);
     }
 
     /**
@@ -933,4 +995,92 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         List<Long> collect = roboticIds.stream().filter(roboticId -> !notFinishAddWxRobotic.contains(roboticId)).collect(Collectors.toList());
         companyVoiceRoboticMapper.finishAddWxRobotic(collect);
     }
+
+    /**
+     * 查询任务执行记录列表
+     * @param roboticId 任务ID
+     * @return 执行记录列表
+     */
+    @Override
+    public List<WorkflowExecRecordVo> getExecRecords(Long roboticId) {
+        // 查询基础执行记录
+        List<WorkflowExecRecordVo> records = companyAiWorkflowExecMapper.selectExecRecordsByRoboticId(roboticId);
+
+        // 补充状态名称和节点日志
+        for (WorkflowExecRecordVo record : records) {
+            // 设置工作流状态名称
+            if (record.getWorkflowStatus() != null) {
+                record.setWorkflowStatusName(getStatusName(record.getWorkflowStatus()));
+            }
+            // 设置节点类型名称
+            if (record.getCurrentNodeType() != null) {
+                record.setCurrentNodeTypeName(getNodeTypeName(record.getCurrentNodeType()));
+            }
+            // 查询节点执行日志
+            if (record.getWorkflowInstanceId() != null) {
+                List<CompanyAiWorkflowExecLog> logs = companyAiWorkflowExecLogMapper.selectByWorkflowInstanceId(record.getWorkflowInstanceId());
+                record.setNodeLogs(convertToNodeLogVos(logs));
+            }
+        }
+
+        return records;
+    }
+
+    /**
+     * 获取工作流状态名称
+     */
+    private String getStatusName(Integer status) {
+        switch (status) {
+            case 1: return "执行成功";
+            case 2: return "执行失败";
+            case 3: return "执行中";
+            case 4: return "已暂停";
+            case 5: return "等待中";
+            case 6: return "已取消";
+            case 7: return "执行超时";
+            default: return "未知";
+        }
+    }
+
+    /**
+     * 获取节点类型名称
+     */
+    private String getNodeTypeName(Integer nodeType) {
+        switch (nodeType) {
+            case 1: return "开始节点";
+            case 2: return "结束节点";
+            case 3: return "条件判断";
+            case 4: return "延时节点";
+            case 5: return "外呼任务";
+            case 6: return "AI外呼电话";
+            case 7: return "AI发送短信";
+            case 8: return "AI添加微信";
+            default: return "未知";
+        }
+    }
+
+    /**
+     * 转换节点日志为VO
+     */
+    private List<WorkflowExecRecordVo.NodeExecLogVo> convertToNodeLogVos(List<CompanyAiWorkflowExecLog> logs) {
+        if (logs == null || logs.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return logs.stream().map(log -> {
+            WorkflowExecRecordVo.NodeExecLogVo vo = new WorkflowExecRecordVo.NodeExecLogVo();
+            vo.setId(log.getId());
+            vo.setNodeKey(log.getNodeKey());
+            vo.setNodeName(log.getNodeName());
+            vo.setNodeType(log.getNodeType());
+            vo.setNodeTypeName(getNodeTypeName(log.getNodeType()));
+            vo.setStatus(log.getStatus());
+            vo.setStatusName(getStatusName(log.getStatus()));
+            vo.setStartTime(log.getStartTime());
+            vo.setEndTime(log.getEndTime());
+            vo.setDuration(log.getDuration());
+            vo.setErrorMessage(log.getErrorMessage());
+            vo.setOutputData(log.getOutputData());
+            return vo;
+        }).collect(Collectors.toList());
+    }
 }

+ 8 - 3
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -99,7 +99,7 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
         try {
             // 加载当前执行记录
             CompanyAiWorkflowExec currentExec =
-                    currentExecutionMapper.selectById(workflowInstanceId);
+                    currentExecutionMapper.selectByWorkflowInstanceId(workflowInstanceId);
 
             if (currentExec == null) {
 //                return ExecutionResult.failure()
@@ -283,7 +283,6 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
             CompanyAiWorkflowExec currentExec = new CompanyAiWorkflowExec();
             currentExec.setWorkflowInstanceId(workflowInstanceId);
             currentExec.setWorkflowId(workflowDefinitionId);
-//        currentExec.setCurrentNodeId(startNodeId);
             currentExec.setCurrentNodeKey(startNodeKey);
             currentExec.setCurrentNodeType(NodeTypeEnum.START.getValue());
             currentExec.setCurrentNodeName(NodeTypeEnum.START.getDescription());
@@ -431,12 +430,18 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
     public void resumeFromBlockingNode(String workflowInstanceId, String nodeKey, Map<String, Object> inputData) {
         try {
             // 加载当前执行记录
-            CompanyAiWorkflowExec currentExec = currentExecutionMapper.selectByWorkflowInstanceIdAndCurrentNode(workflowInstanceId,nodeKey);
+            CompanyAiWorkflowExec currentExec = currentExecutionMapper.selectByWorkflowInstanceId(workflowInstanceId);
 
             if (currentExec == null) {
                 throw new CustomException("工作流实例不存在: " + workflowInstanceId);
             }
 
+            // 验证当前节点是否匹配
+            if (!nodeKey.equals(currentExec.getCurrentNodeKey())) {
+                log.warn("节点不匹配 - 期望: {}, 实际: {}", nodeKey, currentExec.getCurrentNodeKey());
+                throw new CustomException("节点不匹配,期望: " + nodeKey + ", 实际: " + currentExec.getCurrentNodeKey());
+            }
+
             // 检查当前工作流是否处于暂停状态
             if (!Integer.valueOf(ExecutionStatusEnum.PAUSED.getValue()).equals(currentExec.getStatus())) {
                 throw new CustomException("工作流未处于暂停状态,无法唤醒: " + workflowInstanceId);

+ 60 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java

@@ -8,9 +8,13 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.domain.*;
+import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
 import com.fs.company.mapper.CompanyWxAccountMapper;
 import com.fs.company.mapper.CompanyWxClientMapper;
 import com.fs.company.service.*;
+import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
+import com.fs.enums.ExecutionStatusEnum;
+import com.fs.enums.NodeTypeEnum;
 import com.fs.wxcid.domain.CidIpadServerUser;
 import com.fs.wxcid.domain.WxContact;
 import com.fs.wxcid.dto.friend.ContactItem;
@@ -85,6 +89,10 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     private IWxRoomService wxRoomService;
     @Autowired
     private IWxRoomMembersService wxRoomMembersService;
+    @Autowired
+    private CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
+    @Autowired
+    private CompanyWorkflowEngine companyWorkflowEngine;
 
 
 
@@ -524,6 +532,58 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
             companyWxClient.setSuccessAddTime(LocalDateTime.now());
             companyWxClientMapper.updateById(companyWxClient);
             wxContactService.add(companyWxClient.getCustomerId(), companyWxClient.getPhone(), accountId, e);
+
+            // 加微成功后,触发工作流继续执行
+            triggerWorkflowOnAddWxSuccess(companyWxClient.getId());
         });
     }
+
+    /**
+     * 加微成功后触发工作流继续执行
+     * @param wxClientId 加微客户ID
+     */
+    private void triggerWorkflowOnAddWxSuccess(Long wxClientId) {
+        try {
+            // 查找等待中的加微工作流实例
+            CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+                    wxClientId,
+                    ExecutionStatusEnum.WAITING.getValue(),
+                    NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+
+            if (waitingExec == null) {
+                log.info("未找到等待中的加微工作流实例 - wxClientId: {}", wxClientId);
+                return;
+            }
+
+            String workflowInstanceId = waitingExec.getWorkflowInstanceId();
+            String currentNodeKey = waitingExec.getCurrentNodeKey();
+
+            log.info("加微成功回调,尝试触发工作流继续执行 - workflowInstanceId: {}, nodeKey: {}, wxClientId: {}",
+                    workflowInstanceId, currentNodeKey, wxClientId);
+
+            // 互斥检查:如果已经被执行过(超时路径或其他回调),则不再执行
+            if (!AiAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId)) {
+                log.info("工作流已被其他路径执行,跳过 - workflowInstanceId: {}, wxClientId: {}",
+                        workflowInstanceId, wxClientId);
+                return;
+            }
+
+            // 清除超时检测Key(回调成功了,不需要超时检测了)
+            AiAddWxTaskNode.clearTimeoutKey(workflowInstanceId, wxClientId);
+
+            // 触发工作流继续执行
+            Map<String, Object> inputData = new HashMap<>();
+            inputData.put("addWxSuccess", true);
+            inputData.put("wxClientId", wxClientId);
+            inputData.put("triggerType", "callback"); // 回调触发
+
+            companyWorkflowEngine.resumeFromBlockingNode(workflowInstanceId, currentNodeKey, inputData);
+
+            log.info("加微成功回调触发工作流继续执行完成 - workflowInstanceId: {}, wxClientId: {}",
+                    workflowInstanceId, wxClientId);
+
+        } catch (Exception ex) {
+            log.error("加微成功回调触发工作流异常 - wxClientId: {}", wxClientId, ex);
+        }
+    }
 }

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

@@ -85,7 +85,7 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             companyAiWorkflowExecLog.setWorkflowInstanceId(context.getWorkflowInstanceId());
             companyAiWorkflowExecLog.setNodeKey(context.getCurrentNodeKey());
             companyAiWorkflowExecLog.setStatus(ExecutionStatusEnum.PAUSED.getValue());
-            companyAiWorkflowExecLogMapper.selectCompanyAiWorkflowExecLogList(companyAiWorkflowExecLog)
+            companyAiWorkflowExecLogMapper.selectCompanyAiWorkflowExecLogList(companyAiWorkflowExecLog);
         }
     }
 

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

@@ -1,44 +1,170 @@
 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;
 import com.fs.common.utils.spring.SpringUtils;
-import com.fs.company.domain.CompanyAiWorkflowExec;
-import com.fs.company.domain.CompanyWorkflowEdge;
-import com.fs.company.domain.CompanyWorkflowNode;
-import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
-import com.fs.company.mapper.CompanyWorkflowEdgeMapper;
-import com.fs.company.mapper.CompanyWorkflowNodeMapper;
+import com.fs.company.domain.*;
+import com.fs.company.mapper.CompanyWxClientMapper;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.vo.AiAddWxWorkflowConditionVo;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.NodeTypeEnum;
+import lombok.extern.slf4j.Slf4j;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 /**
  * @author MixLiu
  * @date 2026/1/28 13:39
- * @description AI外呼电话任务节点
+ * @description AI添加微信任务节点
  */
+@Slf4j
 public class AiAddWxTaskNode extends AbstractWorkflowNode {
 
-    private static final CompanyWorkflowNodeMapper companyWorkflowNodeMapper = SpringUtils.getBean(CompanyWorkflowNodeMapper.class);
-    private static final CompanyAiWorkflowExecMapper workflowExecMapper = SpringUtils.getBean(CompanyAiWorkflowExecMapper.class);
-    private static final CompanyWorkflowEdgeMapper companyWorkflowEdgeMapper = SpringUtils.getBean(CompanyWorkflowEdgeMapper.class);
+    private static final CompanyWxClientMapper companyWxClientMapper = SpringUtils.getBean(CompanyWxClientMapper.class);
+    @SuppressWarnings("unchecked")
+    private static final RedisCacheT<String> redisCache = SpringUtils.getBean(RedisCacheT.class);
+
+    /**
+     * 默认加微超时时间(分钟)
+     */
+    private static final int DEFAULT_ADD_WX_TIMEOUT_MINUTES = 30;
 
     public AiAddWxTaskNode(String nodeKey, String nodeName, Map<String, Object> properties) {
         super(nodeKey, nodeName, properties);
     }
 
+    /**
+     * 收到加微回调后,继续判定和执行下一步动作
+     *
+     * @param context 执行上下文
+     * @return 执行结果
+     */
     @Override
     protected ExecutionResult doContinue(ExecutionContext context) {
-        return null;
+        CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());
+
+        List<CompanyWorkflowEdge> edges = companyWorkflowEdgeMapper.selectListByWorkflowIdAndNodeKey(exec.getWorkflowId(), nodeKey);
+
+        // 获取业务数据
+        CompanyVoiceRoboticBusiness business = super.getRoboticBusiness(context.getWorkflowInstanceId());
+        // 获取加微记录
+        CompanyWxClient wxClient = companyWxClientMapper.selectById(business.getWxClientId());
+
+        // 根据加微结果判断走哪条边
+        String nextNodeKey = null;
+        for (CompanyWorkflowEdge edge : edges) {
+            if (edge.getConditionExpr() == null || edge.getConditionExpr().isEmpty()) {
+                // 无条件边,直接使用
+                nextNodeKey = edge.getTargetNodeKey();
+                break;
+            }
+            AiAddWxWorkflowConditionVo condition = JSONObject.parseObject(edge.getConditionExpr(), AiAddWxWorkflowConditionVo.class);
+            // 判断加微是否成功 (isAdd: 0否 1是 2待添加 3作废)
+            boolean addSuccess = wxClient != null && Integer.valueOf(1).equals(wxClient.getIsAdd());
+            if (condition.isAddSuccess() == addSuccess) {
+                nextNodeKey = edge.getTargetNodeKey();
+                break;
+            }
+        }
+
+        if (nextNodeKey != null) {
+            return ExecutionResult.success().nextNodeKey(nextNodeKey).build();
+        }
+        return ExecutionResult.failure().errorMessage("未找到满足条件的下一节点").build();
     }
 
+    /**
+     * 执行加微节点逻辑(只准备数据,实际加微由定时任务执行)
+     * 1. 通过 workflowInstanceId 找到 CompanyVoiceRoboticBusiness 业务数据
+     * 2. 验证 CompanyWxClient 数据是否已准备好(accountId、dialogId、isAdd=0)
+     * 3. 设置 Redis 任务状态为 ADD_WX,让定时任务执行加微
+     * 4. 设置超时时间到 Redis
+     * 5. 返回等待状态,等待定时任务执行加微后的回调
+     *
+     * @param context 执行上下文
+     * @return 执行结果
+     */
     @Override
     protected ExecutionResult doExecute(ExecutionContext context) {
-        CompanyWorkflowNode node = companyWorkflowNodeMapper.selectById(context.getCurrentNodeId());
+        try {
+            // 1. 获取业务数据
+            CompanyVoiceRoboticBusiness business = super.getRoboticBusiness(context.getWorkflowInstanceId());
+            if (business == null) {
+                return ExecutionResult.failure().errorMessage("未找到业务数据").build();
+            }
 
-        return null;
+            // 2. 通过 wxClientId 获取加微客户记录
+            Long wxClientId = business.getWxClientId();
+            if (wxClientId == null) {
+                return ExecutionResult.failure().errorMessage("业务数据中缺少wxClientId").build();
+            }
+            CompanyWxClient wxClient = companyWxClientMapper.selectById(wxClientId);
+            if (wxClient == null) {
+                return ExecutionResult.failure().errorMessage("未找到加微客户记录: " + wxClientId).build();
+            }
+
+            // 3. 验证加微数据是否已准备好
+            Long accountId = wxClient.getAccountId();
+            if (accountId == null) {
+                return ExecutionResult.failure().errorMessage("加微客户记录中缺少accountId,请先分配微信账号").build();
+            }
+            Long dialogId = wxClient.getDialogId();
+            if (dialogId == null) {
+                return ExecutionResult.failure().errorMessage("加微客户记录中缺少dialogId,请先设置话术").build();
+            }
+
+            // 4. 确保 isAdd = 0(未添加状态),这样定时任务才会处理
+            if (!Integer.valueOf(0).equals(wxClient.getIsAdd())) {
+                log.warn("加微客户记录状态不是未添加(0),当前状态: {} - wxClientId: {}", wxClient.getIsAdd(), wxClientId);
+                // 如果已经是待添加(2)或已添加(1),说明已经在处理中或已完成
+                if (Integer.valueOf(1).equals(wxClient.getIsAdd())) {
+                    return ExecutionResult.failure().errorMessage("该客户已加微成功,无需重复添加").build();
+                }
+                if (Integer.valueOf(2).equals(wxClient.getIsAdd())) {
+                    // 已经是待添加状态,继续等待
+                    log.info("加微客户记录已是待添加状态,继续等待 - wxClientId: {}", wxClientId);
+                }
+            }
+
+            Long roboticId = business.getRoboticId();
+            log.info("准备加微任务数据 - workflowInstanceId: {}, roboticId: {}, wxClientId: {}, accountId: {}",
+                    context.getWorkflowInstanceId(), roboticId, wxClientId, accountId);
+
+            // 5. 设置 Redis 任务状态为 ADD_WX,让定时任务执行加微
+            // 定时任务 WxTaskService.addWx() 会检查这个状态
+            String taskKey = Constants.TASK_ID + roboticId;
+            redisCache.setCacheObject(taskKey, Constants.ADD_WX);
+            log.info("设置任务状态为加微 - key: {}, value: {}", taskKey, Constants.ADD_WX);
+
+            // 6. 设置加微超时检测时间到 Redis
+            int timeoutMinutes = getTimeoutFromProperties();
+            long timeoutTimestamp = System.currentTimeMillis() + timeoutMinutes * 60 * 1000L;
+            String timeoutKey = Constants.WORKFLOW_ADD_WX_TIMEOUT + context.getWorkflowInstanceId() + ":" + wxClientId;
+            redisCache.setCacheObject(timeoutKey, String.valueOf(timeoutTimestamp));
+            log.info("设置加微超时检测 - key: {}, timeout: {}分钟, 超时时间戳: {}",
+                    timeoutKey, timeoutMinutes, timeoutTimestamp);
+
+            // 7. 保存执行结果到输出数据
+            Map<String, Object> outputData = new HashMap<>();
+            outputData.put("wxClientId", wxClientId);
+            outputData.put("accountId", accountId);
+            outputData.put("roboticId", roboticId);
+            outputData.put("preparedForAddWx", true);
+
+            // 8. 异步节点返回等待状态,等待定时任务执行加微后的回调
+            return ExecutionResult.paused()
+                    .outputData(outputData)
+                    .nextNodeKey(getNextNodeKey(context.getWorkflowInstanceId(), nodeKey))
+                    .build();
+        } catch (Exception e) {
+            log.error("准备加微任务数据异常 - workflowInstanceId: {}", context.getWorkflowInstanceId(), e);
+            return ExecutionResult.failure().errorMessage("准备加微任务数据异常: " + e.getMessage()).build();
+        }
     }
 
     @Override
@@ -53,20 +179,94 @@ public class AiAddWxTaskNode extends AbstractWorkflowNode {
 
     @Override
     public String getNextNodeKey(String workflowInstanceId, String nodeKey) {
-
-        CompanyAiWorkflowExec companyAiWorkflowExec = workflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
+        CompanyAiWorkflowExec companyAiWorkflowExec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
 
         List<CompanyWorkflowEdge> companyWorkflowEdges =
                 companyWorkflowEdgeMapper.selectListByWorkflowIdAndNodeKey(companyAiWorkflowExec.getWorkflowId(), nodeKey);
-        CompanyWorkflowEdge result = null;
-        for (CompanyWorkflowEdge companyWorkflowEdge : companyWorkflowEdges) {
 
+        // 多条边时需要根据条件判断
+        if (companyWorkflowEdges != null && !companyWorkflowEdges.isEmpty()) {
+            if (companyWorkflowEdges.size() > 1) {
+                // 存在多条件时,找默认边或第一条边
+                for (CompanyWorkflowEdge edge : companyWorkflowEdges) {
+                    if (edge.getConditionExpr() == null || edge.getConditionExpr().isEmpty()) {
+                        return edge.getTargetNodeKey();
+                    }
+                }
+                // 如果没有无条件边,返回第一条
+                return companyWorkflowEdges.get(0).getTargetNodeKey();
+            } else {
+                return companyWorkflowEdges.get(0).getTargetNodeKey();
+            }
         }
-        return result == null ? null : result.getTargetNodeKey();
+        return null;
     }
+
     @Override
-    public Boolean edgeConditionValidate(String condition){
+    public Boolean edgeConditionValidate(String condition) {
+        if (condition == null || condition.isEmpty()) {
+            return true;
+        }
+        try {
+            AiAddWxWorkflowConditionVo conditionVo = JSON.parseObject(condition, AiAddWxWorkflowConditionVo.class);
+            return conditionVo != null;
+        } catch (Exception e) {
+            log.error("解析加微条件失败: {}", condition, e);
+            return false;
+        }
+    }
+
+    /**
+     * 从节点配置获取超时时间(分钟)
+     */
+    private int getTimeoutFromProperties() {
+        if (properties != null && properties.containsKey("timeout")) {
+            Object timeout = properties.get("timeout");
+            if (timeout instanceof Number) {
+                return ((Number) timeout).intValue();
+            }
+            if (timeout instanceof String) {
+                try {
+                    return Integer.parseInt((String) timeout);
+                } catch (NumberFormatException e) {
+                    log.warn("解析超时时间失败: {}, 使用默认值: {}", timeout, DEFAULT_ADD_WX_TIMEOUT_MINUTES);
+                }
+            }
+        }
+        return DEFAULT_ADD_WX_TIMEOUT_MINUTES;
+    }
+
+    /**
+     * 检查并标记已执行(用于互斥逻辑)
+     * 如果返回 true 表示当前是第一个执行的,可以继续
+     * 如果返回 false 表示已经被其他路径执行过了,不再执行
+     *
+     * @param workflowInstanceId 工作流实例ID
+     * @param wxClientId 加微客户ID
+     * @return 是否可以执行
+     */
+    public static boolean tryMarkAsExecuted(String workflowInstanceId, Long wxClientId) {
+        String executedKey = Constants.WORKFLOW_ADD_WX_EXECUTED + workflowInstanceId + ":" + wxClientId;
+        String existingValue = redisCache.getCacheObject(executedKey);
+        if (existingValue != null) {
+            // 已经被执行过了
+            return false;
+        }
+        // 标记为已执行,设置1小时过期
+        redisCache.setCacheObject(executedKey, "1");
+        redisCache.expire(executedKey, 3600);
+        return true;
+    }
 
-        return false;
+    /**
+     * 清除超时检测 Key
+     *
+     * @param workflowInstanceId 工作流实例ID
+     * @param wxClientId 加微客户ID
+     */
+    public static void clearTimeoutKey(String workflowInstanceId, Long wxClientId) {
+        String timeoutKey = Constants.WORKFLOW_ADD_WX_TIMEOUT + workflowInstanceId + ":" + wxClientId;
+        redisCache.deleteObject(timeoutKey);
+        log.info("清除加微超时检测 Key: {}", timeoutKey);
     }
 }

+ 33 - 0
fs-service/src/main/java/com/fs/company/vo/AiAddWxWorkflowConditionVo.java

@@ -0,0 +1,33 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+/**
+ * AI添加微信工作流条件VO
+ * 用于判断加微节点的流转条件
+ *
+ * @author MixLiu
+ * @date 2026/1/29
+ */
+@Data
+public class AiAddWxWorkflowConditionVo {
+
+    /**
+     * 加微是否成功
+     * true: 加微成功
+     * false: 加微失败
+     */
+    private boolean addSuccess;
+
+    /**
+     * 超时时间(分钟)
+     * 加微后等待确认的超时时间
+     */
+    private Integer timeout;
+
+    /**
+     * 重试次数
+     * 加微失败后的重试次数限制
+     */
+    private Integer retryCount;
+}

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

@@ -0,0 +1,189 @@
+package com.fs.company.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 工作流执行记录VO
+ * 用于前端展示每个人的执行记录
+ *
+ * @author MixLiu
+ * @date 2026/1/29
+ */
+@Data
+public class WorkflowExecRecordVo {
+
+    /**
+     * 业务ID (company_voice_robotic_business.id)
+     */
+    private Long businessId;
+
+    /**
+     * 任务ID
+     */
+    private Long roboticId;
+
+    /**
+     * 外呼ID
+     */
+    private Long calleeId;
+
+    /**
+     * 客户ID
+     */
+    private Long customerId;
+
+    /**
+     * 客户姓名
+     */
+    private String customerName;
+
+    /**
+     * 客户手机号
+     */
+    private String customerPhone;
+
+    /**
+     * 加微客户ID
+     */
+    private Long wxClientId;
+
+    /**
+     * 工作流实例ID
+     */
+    private String workflowInstanceId;
+
+    /**
+     * 工作流状态 (1:成功 2:失败 3:执行中 4:暂停 5:等待 6:取消 7:超时)
+     */
+    private Integer workflowStatus;
+
+    /**
+     * 工作流状态名称
+     */
+    private String workflowStatusName;
+
+    /**
+     * 当前节点Key
+     */
+    private String currentNodeKey;
+
+    /**
+     * 当前节点名称
+     */
+    private String currentNodeName;
+
+    /**
+     * 当前节点类型
+     */
+    private Integer currentNodeType;
+
+    /**
+     * 当前节点类型名称
+     */
+    private String currentNodeTypeName;
+
+    /**
+     * 开始时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime startTime;
+
+    /**
+     * 最后更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime lastUpdateTime;
+
+    /**
+     * 加微完成次数
+     */
+    private Integer addWxDone;
+
+    /**
+     * 打电话完成次数
+     */
+    private Integer callPhoneDone;
+
+    /**
+     * 发短信完成次数
+     */
+    private Integer sendMsgDone;
+
+    /**
+     * 节点执行日志列表
+     */
+    private List<NodeExecLogVo> nodeLogs;
+
+    /**
+     * 节点执行日志VO
+     */
+    @Data
+    public static class NodeExecLogVo {
+        /**
+         * 日志ID
+         */
+        private Long id;
+
+        /**
+         * 节点Key
+         */
+        private String nodeKey;
+
+        /**
+         * 节点名称
+         */
+        private String nodeName;
+
+        /**
+         * 节点类型
+         */
+        private Integer nodeType;
+
+        /**
+         * 节点类型名称
+         */
+        private String nodeTypeName;
+
+        /**
+         * 状态
+         */
+        private Integer status;
+
+        /**
+         * 状态名称
+         */
+        private String statusName;
+
+        /**
+         * 开始时间
+         */
+        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+        private Date startTime;
+
+        /**
+         * 结束时间
+         */
+        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+        private Date endTime;
+
+        /**
+         * 执行时长(毫秒)
+         */
+        private Long duration;
+
+        /**
+         * 错误信息
+         */
+        private String errorMessage;
+
+        /**
+         * 输出数据
+         */
+        private String outputData;
+    }
+}

+ 1 - 5
fs-service/src/main/java/com/fs/enums/ExecutionStatusEnum.java

@@ -39,11 +39,7 @@ public enum ExecutionStatusEnum {
     /**
      * 超时
      */
-    TIMEOUT("TIMEOUT", "执行超时", 7),
-    /**
-     * 中断
-     */
-    TIMEOUT("TIMEOUT", "执行中断", 7);
+    TIMEOUT("TIMEOUT", "执行超时", 7);
 
     private final String code;
     private final String description;

+ 7 - 0
fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecLogMapper.xml

@@ -115,4 +115,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
         </foreach>
     </delete>
+
+    <!-- 根据工作流实例ID查询执行日志 -->
+    <select id="selectByWorkflowInstanceId" resultType="CompanyAiWorkflowExecLog">
+        SELECT * FROM company_ai_workflow_exec_log
+        WHERE workflow_instance_id = #{workflowInstanceId}
+        ORDER BY start_time ASC
+    </select>
 </mapper>

+ 45 - 6
fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecMapper.xml

@@ -9,7 +9,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="workflowInstanceId"    column="workflow_instance_id"    />
         <result property="workflowId"    column="workflow_id"    />
         <result property="workflowVersion"    column="workflow_version"    />
-        <result property="currentNodeId"    column="current_node_id"    />
+        <result property="currentNodeKey"    column="current_node_key"    />
         <result property="currentNodeName"    column="current_node_name"    />
         <result property="currentNodeType"    column="current_node_type"    />
         <result property="status"    column="status"    />
@@ -20,7 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectCompanyAiWorkflowExecVo">
-        select id, workflow_instance_id, workflow_id, workflow_version, current_node_id, current_node_name, current_node_type, status, variables, start_time, last_update_time, business_key from company_ai_workflow_exec
+        select id, workflow_instance_id, workflow_id, workflow_version, current_node_key, current_node_name, current_node_type, status, variables, start_time, last_update_time, business_key from company_ai_workflow_exec
     </sql>
 
     <select id="selectCompanyAiWorkflowExecList" parameterType="CompanyAiWorkflowExec" resultMap="CompanyAiWorkflowExecResult">
@@ -52,7 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="workflowInstanceId != null">workflow_instance_id,</if>
             <if test="workflowId != null">workflow_id,</if>
             <if test="workflowVersion != null">workflow_version,</if>
-            <if test="currentNodeId != null">current_node_id,</if>
+            <if test="currentNodeKey != null">current_node_key,</if>
             <if test="currentNodeName != null">current_node_name,</if>
             <if test="currentNodeType != null">current_node_type,</if>
             <if test="status != null">status,</if>
@@ -66,7 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="workflowInstanceId != null">#{workflowInstanceId},</if>
             <if test="workflowId != null">#{workflowId},</if>
             <if test="workflowVersion != null">#{workflowVersion},</if>
-            <if test="currentNodeId != null">#{currentNodeId},</if>
+            <if test="currentNodeKey != null">#{currentNodeKey},</if>
             <if test="currentNodeName != null">#{currentNodeName},</if>
             <if test="currentNodeType != null">#{currentNodeType},</if>
             <if test="status != null">#{status},</if>
@@ -83,7 +83,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="workflowInstanceId != null">workflow_instance_id = #{workflowInstanceId},</if>
             <if test="workflowId != null">workflow_id = #{workflowId},</if>
             <if test="workflowVersion != null">workflow_version = #{workflowVersion},</if>
-            <if test="currentNodeId != null">current_node_id = #{currentNodeId},</if>
+            <if test="currentNodeKey != null">current_node_key = #{currentNodeKey},</if>
             <if test="currentNodeName != null">current_node_name = #{currentNodeName},</if>
             <if test="currentNodeType != null">current_node_type = #{currentNodeType},</if>
             <if test="status != null">status = #{status},</if>
@@ -100,7 +100,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="SET" suffixOverrides=",">
             <if test="workflowId != null">workflow_id = #{workflowId},</if>
             <if test="workflowVersion != null">workflow_version = #{workflowVersion},</if>
-            <if test="currentNodeId != null">current_node_id = #{currentNodeId},</if>
+            <if test="currentNodeKey != null">current_node_key = #{currentNodeKey},</if>
             <if test="currentNodeName != null">current_node_name = #{currentNodeName},</if>
             <if test="currentNodeType != null">current_node_type = #{currentNodeType},</if>
             <if test="status != null">status = #{status},</if>
@@ -129,4 +129,43 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectByWorkflowInstanceIdAndCurrentNode" resultType="CompanyAiWorkflowExec">
         select * from company_ai_workflow_exec where workflow_instance_id = #{workflowInstanceId} and current_node_key = #{currentNode}
     </select>
+
+    <!-- 根据wxClientId查找等待中的加微工作流实例 -->
+    <select id="selectWaitingAddWxWorkflowByWxClientId" resultType="CompanyAiWorkflowExec">
+        SELECT t1.*
+        FROM company_ai_workflow_exec t1
+        INNER JOIN company_voice_robotic_business t2 ON t1.business_key = t2.id
+        WHERE t2.wx_client_id = #{wxClientId}
+          AND t1.status = #{status}
+          AND t1.current_node_type = #{nodeType}
+        ORDER BY t1.last_update_time DESC
+        LIMIT 1
+    </select>
+
+    <!-- 根据任务ID查询执行记录列表 -->
+    <select id="selectExecRecordsByRoboticId" resultType="com.fs.company.vo.WorkflowExecRecordVo">
+        SELECT
+            b.id AS businessId,
+            b.robotic_id AS roboticId,
+            b.callee_id AS calleeId,
+            c.user_id AS customerId,
+            c.user_name AS customerName,
+            c.phone AS customerPhone,
+            b.wx_client_id AS wxClientId,
+            e.workflow_instance_id AS workflowInstanceId,
+            e.status AS workflowStatus,
+            e.current_node_key AS currentNodeKey,
+            e.current_node_name AS currentNodeName,
+            e.current_node_type AS currentNodeType,
+            e.start_time AS startTime,
+            e.last_update_time AS lastUpdateTime,
+            b.add_wx_done AS addWxDone,
+            b.call_phone_done AS callPhoneDone,
+            b.send_msg_done AS sendMsgDone
+        FROM company_voice_robotic_business b
+        LEFT JOIN company_ai_workflow_exec e ON b.id = e.business_key
+        LEFT JOIN company_voice_robotic_callees c ON b.callee_id = c.id
+        WHERE b.robotic_id = #{roboticId}
+        ORDER BY e.start_time DESC
+    </select>
 </mapper>

+ 4 - 1
fs-service/src/main/resources/mapper/company/CompanyWorkflowMapper.xml

@@ -10,6 +10,8 @@
         <result property="status" column="status"/>
         <result property="version" column="version"/>
         <result property="canvasData" column="canvas_data"/>
+        <result property="startNodeKey" column="start_node_key"/>
+        <result property="endNodeKey" column="end_node_key"/>
         <result property="createBy" column="create_by"/>
         <result property="createTime" column="create_time"/>
         <result property="updateBy" column="update_by"/>
@@ -17,11 +19,12 @@
         <result property="remark" column="remark"/>
         <result property="delFlag" column="del_flag"/>
         <result property="companyUserId" column="company_user_id"/>
+        <result property="companyId" column="company_id"/>
     </resultMap>
 
     <sql id="selectCompanyWorkflowVo">
         select workflow_id, workflow_name, workflow_desc, workflow_type, status, version,
-               canvas_data, create_by, create_time, update_by, update_time, remark, del_flag, company_user_id
+               canvas_data, start_node_key, end_node_key, create_by, create_time, update_by, update_time, remark, del_flag, company_user_id, company_id
         from company_ai_workflow
     </sql>
     <update id="deleteAiWorkflowCompanyUserVoice" >

+ 107 - 0
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -18,6 +18,10 @@ import com.fs.company.service.ICompanyWxAccountService;
 import com.fs.company.service.ICompanyWxClientService;
 import com.fs.company.service.ICompanyWxDialogService;
 import com.fs.company.service.impl.*;
+import com.fs.company.service.CompanyWorkflowEngine;
+import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
+import com.fs.enums.ExecutionStatusEnum;
+import com.fs.enums.NodeTypeEnum;
 import com.fs.company.util.ObjectPlaceholderResolver;
 import com.fs.company.vo.SendMsgVo;
 import com.fs.course.config.WxConfig;
@@ -66,6 +70,8 @@ public class WxTaskService {
     private RedissonClient redissonClient;
     private final CompanyVoiceRoboticServiceImpl companyVoiceRoboticServiceImpl;
     private final CompanyVoiceRoboticCallLogCallphoneServiceImpl companyVoiceRoboticCallLogCallphoneService;
+    private final CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
+    private final CompanyWorkflowEngine companyWorkflowEngine;
     private final ExecutorService cidExcutor = new ThreadPoolExecutor(
             32,
             64,
@@ -587,4 +593,105 @@ public class WxTaskService {
             log.error("记录任务执行日志失败:失败数据:{}",logAddwx, ex);
         }
     }
+
+    /**
+     * 工作流加微超时检测
+     * 扫描Redis中的加微超时检测Key,如果超时则触发工作流继续执行
+     */
+    public void checkWorkflowAddWxTimeout() {
+        RLock lock = redissonClient.getLock("WORKFLOW_ADD_WX_TIMEOUT_CHECK");
+        try {
+            lock.lock();
+            log.info("===========工作流加微超时检测开始===========");
+            long currentTime = System.currentTimeMillis();
+
+            // 扫描所有加微超时检测Key
+            Collection<String> keys = redisCache.keys(Constants.WORKFLOW_ADD_WX_TIMEOUT + "*");
+            if (keys == null || keys.isEmpty()) {
+                log.info("没有待检测的加微超时Key");
+                return;
+            }
+
+            log.info("找到 {} 个待检测的加微超时Key", keys.size());
+
+            keys.parallelStream().forEach(key -> {
+                try {
+                    // 解析Key: workflow:addwx:timeout:{workflowInstanceId}:{wxClientId}
+                    String[] keyParts = key.split(":");
+                    if (keyParts.length < 5) {
+                        log.warn("无效的加微超时Key格式: {}", key);
+                        return;
+                    }
+                    String workflowInstanceId = keyParts[keyParts.length - 2];
+                    Long wxClientId = Long.parseLong(keyParts[keyParts.length - 1]);
+
+                    // 获取超时时间戳
+                    String timeoutStr = redisCache.getCacheObject(key);
+                    if (StringUtils.isBlank(timeoutStr)) {
+                        log.warn("加微超时Key值为空: {}", key);
+                        redisCache.deleteObject(key);
+                        return;
+                    }
+                    long timeoutTimestamp = Long.parseLong(timeoutStr);
+
+                    // 检查是否超时
+                    if (currentTime < timeoutTimestamp) {
+                        // 还没到超时时间
+                        return;
+                    }
+
+                    log.info("加微超时,准备触发工作流继续执行 - workflowInstanceId: {}, wxClientId: {}",
+                            workflowInstanceId, wxClientId);
+
+                    // 互斥检查:如果已经被执行过(回调成功路径),则不再执行
+                    if (!AiAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId)) {
+                        log.info("工作流已被其他路径执行,跳过超时处理 - workflowInstanceId: {}, wxClientId: {}",
+                                workflowInstanceId, wxClientId);
+                        // 清除超时Key
+                        redisCache.deleteObject(key);
+                        return;
+                    }
+
+                    // 清除超时Key
+                    redisCache.deleteObject(key);
+
+                    // 查找等待中的加微工作流实例
+                    CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+                            wxClientId,
+                            ExecutionStatusEnum.WAITING.getValue(),
+                            NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+
+                    if (waitingExec == null) {
+                        log.info("未找到等待中的加微工作流实例 - wxClientId: {}", wxClientId);
+                        return;
+                    }
+
+                    String currentNodeKey = waitingExec.getCurrentNodeKey();
+
+                    // 触发工作流继续执行(超时路径)
+                    Map<String, Object> inputData = new HashMap<>();
+                    inputData.put("addWxSuccess", false);  // 超时意味着加微未成功
+                    inputData.put("wxClientId", wxClientId);
+                    inputData.put("triggerType", "timeout"); // 超时触发
+
+                    companyWorkflowEngine.resumeFromBlockingNode(workflowInstanceId, currentNodeKey, inputData);
+
+                    log.info("加微超时触发工作流继续执行完成 - workflowInstanceId: {}, wxClientId: {}",
+                            workflowInstanceId, wxClientId);
+
+                } catch (Exception ex) {
+                    log.error("处理加微超时检测异常 - key: {}", key, ex);
+                }
+            });
+
+            log.info("===========工作流加微超时检测结束===========");
+
+        } catch (Exception ex) {
+            log.error("工作流加微超时检测任务异常", ex);
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
 }

+ 9 - 0
fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

@@ -41,4 +41,13 @@ public class WxTask {
     public void callNextTask(){
         taskService.callNextTask();
     }
+
+    /**
+     * 工作流加微超时检测
+     * 每分钟执行一次,检查是否有加微超时的工作流需要继续执行
+     */
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void checkWorkflowAddWxTimeout(){
+        taskService.checkWorkflowAddWxTimeout();
+    }
 }