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

Merge remote-tracking branch 'origin/master'

yh 1 месяц назад
Родитель
Сommit
834ca8985e
93 измененных файлов с 4564 добавлено и 372 удалено
  1. 31 0
      fs-common/src/main/java/com/fs/common/annotation/CallbackIpCheck.java
  2. 107 0
      fs-company/src/main/java/com/fs/company/aspectj/CallbackIpCheckAspect.java
  3. 318 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java
  4. 9 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceCloneController.java
  5. 35 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java
  6. 48 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  7. 24 2
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  8. 16 4
      fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java
  9. 1 1
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java
  10. 30 0
      fs-service/src/main/java/com/fs/company/domain/CompanyInboundBind.java
  11. 42 0
      fs-service/src/main/java/com/fs/company/domain/CompanySiptaskInfo.java
  12. 42 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceCloneRef.java
  13. 6 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java
  14. 5 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWxAccount.java
  15. 111 0
      fs-service/src/main/java/com/fs/company/domain/EasyCallInboundCdrVO.java
  16. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecMapper.java
  17. 67 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyInboundBindMapper.java
  18. 65 0
      fs-service/src/main/java/com/fs/company/mapper/CompanySiptaskInfoMapper.java
  19. 33 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceCloneRefMapper.java
  20. 20 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogAddwxMapper.java
  21. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWxAccountMapper.java
  22. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWxClientMapper.java
  23. 167 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java
  24. 5 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallMapper.java
  25. 21 0
      fs-service/src/main/java/com/fs/company/param/AddWxActionParam.java
  26. 19 0
      fs-service/src/main/java/com/fs/company/param/InboundCallbackParam.java
  27. 6 0
      fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java
  28. 78 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  29. 15 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneRefService.java
  30. 1 1
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneService.java
  31. 13 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogAddwxService.java
  32. 3 0
      fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java
  33. 13 0
      fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java
  34. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/AsyncCalleeProcessorServiceImpl.java
  35. 174 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java
  36. 37 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneRefServiceImpl.java
  37. 47 3
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneServiceImpl.java
  38. 16 3
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogAddwxServiceImpl.java
  39. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  40. 133 16
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  41. 85 9
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  42. 64 43
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java
  43. 120 0
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  44. 41 10
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java
  45. 326 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java
  46. 81 40
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  47. 18 16
      fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java
  48. 3 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/WorkflowNodeFactory.java
  49. 202 0
      fs-service/src/main/java/com/fs/company/util/IpCheckUtil.java
  50. 2 0
      fs-service/src/main/java/com/fs/company/vo/AiCallConfigVO.java
  51. 4 0
      fs-service/src/main/java/com/fs/company/vo/CidConfigVO.java
  52. 43 0
      fs-service/src/main/java/com/fs/company/vo/InboundCallInfo.java
  53. 2 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCreateTaskParam.java
  54. 22 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java
  55. 5 1
      fs-service/src/main/java/com/fs/enums/NodeTypeEnum.java
  56. 18 23
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  57. 17 0
      fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java
  58. 16 0
      fs-service/src/main/java/com/fs/his/dto/SendResultDetailDTO.java
  59. 2 0
      fs-service/src/main/java/com/fs/hisStore/vo/StoreOperMainVO.java
  60. 20 0
      fs-service/src/main/java/com/fs/ipad/vo/WxBaseVo.java
  61. 9 0
      fs-service/src/main/java/com/fs/ipad/vo/WxTxtVo.java
  62. 2 0
      fs-service/src/main/java/com/fs/live/vo/DateRange.java
  63. 11 4
      fs-service/src/main/java/com/fs/wxcid/domain/WxContact.java
  64. 38 0
      fs-service/src/main/java/com/fs/wxcid/dto/callback/WxCallbackVo.java
  65. 68 0
      fs-service/src/main/java/com/fs/wxcid/service/IWxMsgLogService.java
  66. 131 0
      fs-service/src/main/java/com/fs/wxcid/service/impl/WxMsgLogServiceImpl.java
  67. 15 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/AddResultWxVo.java
  68. 19 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/AddWxVo.java
  69. 18 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/ContactInfoVo.java
  70. 16 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/MsgResultVo.java
  71. 16 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/SyncInfoVo.java
  72. 15 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/WxSendMsgVo.java
  73. 15 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/WxSendResultMsgVo.java
  74. 58 0
      fs-service/src/main/java/com/fs/wxwork/service/WxIpadService.java
  75. 307 0
      fs-service/src/main/java/com/fs/wxwork/utils/WxHttpUtil.java
  76. 4 4
      fs-service/src/main/resources/application-dev.yml
  77. 26 0
      fs-service/src/main/resources/mapper/company/CompanyAiWorkflowExecMapper.xml
  78. 77 0
      fs-service/src/main/resources/mapper/company/CompanyInboundBindMapper.xml
  79. 79 0
      fs-service/src/main/resources/mapper/company/CompanySiptaskInfoMapper.xml
  80. 100 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceCloneRefMapper.xml
  81. 55 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogAddwxMapper.xml
  82. 3 2
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml
  83. 3 1
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogSendmsgMapper.xml
  84. 4 0
      fs-service/src/main/resources/mapper/company/CompanyWxAccountMapper.xml
  85. 4 0
      fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml
  86. 323 0
      fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml
  87. 12 0
      fs-service/src/main/resources/mapper/company/EasyCallMapper.xml
  88. 49 70
      fs-wx-api/src/main/java/com/fs/app/controller/CommonController.java
  89. 28 0
      fs-wx-api/src/main/java/com/fs/app/enums/CmdType.java
  90. 35 0
      fs-wx-api/src/main/java/com/fs/app/websocket/bean/ResultMsgVo.java
  91. 6 4
      fs-wx-api/src/main/java/com/fs/app/websocket/bean/SendMsgVo.java
  92. 150 102
      fs-wx-api/src/main/java/com/fs/app/websocket/service/WebSocketServer.java
  93. 8 8
      fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

+ 31 - 0
fs-common/src/main/java/com/fs/common/annotation/CallbackIpCheck.java

@@ -0,0 +1,31 @@
+package com.fs.common.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 回调来源IP校验注解
+ * <p>
+ * 标注在Controller方法上,自动校验请求来源IP是否在配置的合法IP列表中。
+ * 切面会从方法参数中提取companyId,查询company_config表获取配置,解析出legalIPs后进行校验。
+ * </p>
+ * <pre>
+ * 使用示例:
+ *   @PostMapping("/inboundCallback")
+ *   @CallbackIpCheck
+ *   public String inboundCallback(@RequestBody InboundCallbackParam param) {
+ *       // 业务逻辑 - IP校验已由切面自动完成
+ *   }
+ * </pre>
+ *
+ * @author MixLiu
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface CallbackIpCheck {
+
+    /**
+     * 公司配置的键名,用于从company_config表查询配置
+     */
+    String configKey() default "cId.config";
+}

+ 107 - 0
fs-company/src/main/java/com/fs/company/aspectj/CallbackIpCheckAspect.java

@@ -0,0 +1,107 @@
+package com.fs.company.aspectj;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.aicall.utils.StringUtils;
+import com.fs.common.annotation.CallbackIpCheck;
+import com.fs.common.utils.IpUtil;
+import com.fs.company.mapper.CompanyConfigMapper;
+import com.fs.company.util.IpCheckUtil;
+import com.fs.company.vo.CidConfigVO;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+
+/**
+ * 回调IP校验切面
+ * <p>
+ * 拦截所有标注了 @CallbackIpCheck 的方法,自动完成:
+ * 1. 从方法参数中提取 companyId
+ * 2. 查询 company_config 获取配置(如 cId.config)
+ * 3. 解析 CidPhoneConfig 获取 legalIPs
+ * 4. 校验请求来源IP是否在合法列表中
+ * 5. 不合法则阻断请求
+ * </p>
+ *
+ * @author MixLiu
+ */
+@Aspect
+@Component
+public class CallbackIpCheckAspect {
+
+    private static final Logger log = LoggerFactory.getLogger(CallbackIpCheckAspect.class);
+
+    @Autowired
+    private CompanyConfigMapper companyConfigMapper;
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+
+    @Pointcut("@annotation(com.fs.common.annotation.CallbackIpCheck)")
+    public void ipCheckPointCut() {
+    }
+
+    @Around("ipCheckPointCut()")
+    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+        // 获取注解
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        CallbackIpCheck annotation = method.getAnnotation(CallbackIpCheck.class);
+
+        // 获取当前请求
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes == null) {
+            throw new IllegalStateException("CallbackIpCheck: 无法获取当前请求上下文");
+        }
+        HttpServletRequest request = attributes.getRequest();
+        String clientIp = IpUtil.getRequestIp(request);
+
+        // 查询配置
+        String configKey = annotation.configKey();
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey(configKey);
+        if(null == sysConfig || StringUtils.isBlank(sysConfig.getConfigValue())){
+            log.error("CallbackIpCheck: 未找到配置,  configKey={}, 请求IP: {}",
+                    configKey, clientIp);
+            throw new IllegalArgumentException("CallbackIpCheck: 未找到公司配置");
+        }
+
+        CidConfigVO cidConf;
+        try {
+            cidConf = JSONObject.parseObject(sysConfig.getConfigValue(), CidConfigVO.class);
+        } catch (Exception e) {
+            log.error("CallbackIpCheck: 配置JSON解析失败,  configValue={}",
+                    sysConfig.getConfigValue(), e);
+            throw new IllegalArgumentException("CallbackIpCheck: 配置解析异常");
+        }
+
+        String legalIPs = cidConf.getLegalIPs();
+
+        // 校验IP
+        if (!IpCheckUtil.isIpInList(clientIp, legalIPs)) {
+            log.warn("非法回调来源IP: {}, legalIPs: {}", clientIp, legalIPs);
+            // 根据目标方法的返回类型返回对应的错误响应
+            Class<?> returnType = method.getReturnType();
+            if (returnType == String.class) {
+                return "illegal IP";
+            }
+            // 非String返回类型则抛异常,由全局异常处理器处理
+            throw new SecurityException("非法IP来源,请求IP: " + clientIp);
+        }
+
+        // IP校验通过,放行
+        return joinPoint.proceed();
+    }
+
+
+}

+ 318 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java

@@ -0,0 +1,318 @@
+package com.fs.company.controller.company;
+
+import com.fs.aicall.service.ICompanyBindAiModelService;
+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.StringUtils;
+import com.fs.company.domain.CompanyInboundBind;
+import com.fs.company.domain.EasyCallInboundCdrVO;
+import com.fs.company.mapper.CompanyInboundBindMapper;
+import com.fs.company.mapper.EasyCallInboundLlmMapper;
+import com.fs.company.service.ICompanyInboundCallManageService;
+import com.fs.company.vo.easycall.*;
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 呼入大模型配置 Controller
+ *
+ * @author fs
+ */
+@RestController
+@RequestMapping("/company/inboundCallManage")
+public class CompanyInboundCallManageController extends BaseController {
+
+    @Autowired
+    private ICompanyInboundCallManageService inboundCallManageService;
+
+    @Autowired
+    private EasyCallInboundLlmMapper inboundLlmMapper;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    CompanyInboundBindMapper companyInboundBindMapper;
+
+    @Autowired
+    private ICompanyBindAiModelService companyBindAiModelService;
+
+    /**
+     * 查询呼入大模型配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('inboundCallManage:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(EasyCallInboundLlmVO vo) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CompanyInboundBind> companyInboundBinds = companyInboundBindMapper.selectIdsByCompanyId(loginUser.getUser().getCompanyId());
+        if (null == companyInboundBinds || companyInboundBinds.isEmpty()) {
+            return getDataTable(null);
+        }
+        List<Long> ids = companyInboundBinds.stream().map(companyInboundBind -> companyInboundBind.getInboundLlmAccountId()).collect(Collectors.toList());
+        vo.setVisibleIds(ids);
+        startPage();
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmList(vo);
+        TableDataInfo rspData = getDataTable(list);
+        // 填充关联数据
+        @SuppressWarnings("unchecked")
+        List<EasyCallInboundLlmVO> records = (List<EasyCallInboundLlmVO>) rspData.getRows();
+        for (EasyCallInboundLlmVO data : records) {
+            fillRelationData(data);
+        }
+        rspData.setRows(records);
+        return rspData;
+    }
+
+    /**
+     * 获取大模型账户下拉列表
+     */
+    @GetMapping("/llmAccountList")
+    public AjaxResult getLlmAccountList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        // 获取当前登录的公司ID
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<Long> modelsId = new ArrayList<>();
+        if (companyId != null) {
+            List<Long> modelIds = companyBindAiModelService.selectModelIdsByCompanyId(companyId);
+            if (!modelIds.isEmpty()) {
+                modelsId = modelIds;
+            } else {
+                return AjaxResult.success();
+            }
+        }
+        List<EasyCallLlmAccountVO> list = inboundLlmMapper.selectLlmAccountList(modelsId);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取呼入大模型配置详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Integer id) {
+        return AjaxResult.success(inboundCallManageService.selectInboundLlmById(id));
+    }
+
+    /**
+     * 新增呼入大模型配置
+     */
+    @PreAuthorize("@ss.hasPermi('inboundCallManage:add')")
+    @Log(title = "呼入大模型配置", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody EasyCallInboundLlmVO vo) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        vo.setCompanyId(loginUser.getUser().getCompanyId());
+        return toAjax(inboundCallManageService.insertInboundLlm(vo));
+    }
+
+    /**
+     * 修改呼入大模型配置
+     */
+    @PreAuthorize("@ss.hasPermi('inboundCallManage:edit')")
+    @Log(title = "呼入大模型配置", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody EasyCallInboundLlmVO vo) {
+        return toAjax(inboundCallManageService.updateInboundLlm(vo));
+    }
+
+    /**
+     * 删除呼入大模型配置
+     */
+    @Log(title = "呼入大模型配置", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPermi('inboundCallManage:delete')")
+    public AjaxResult remove(@PathVariable String ids) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        return toAjax(inboundCallManageService.deleteInboundLlmByIds(ids,companyId));
+    }
+
+    /**
+     * 校验被叫号码是否唯一
+     */
+    @GetMapping("/checkCallee")
+    public AjaxResult checkCallee(@RequestParam(value = "id", required = false) Integer id,
+                                  @RequestParam("callee") String callee) {
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmByCallee(callee);
+        if (list.size() <= 0) {
+            return AjaxResult.success(true);
+        }
+        if (null != id && list.get(0).getId().equals(id)) {
+            return AjaxResult.success(true);
+        }
+        return AjaxResult.success(false);
+    }
+
+    /**
+     * 获取所有AI配置列表
+     */
+    @GetMapping("/ai/all")
+    public AjaxResult getAllAi() {
+        EasyCallInboundLlmVO query = new EasyCallInboundLlmVO();
+        query.setServiceType("ai");
+        List<EasyCallInboundLlmVO> list = inboundCallManageService.selectInboundLlmList(query);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取ASR提供商列表
+     */
+    @GetMapping("/asrProviderList")
+    public AjaxResult getAsrProviderList() {
+        List<Map<String, String>> list = inboundLlmMapper.selectAsrProviderList();
+        // 转换为Map格式
+        Map<String, String> result = new HashMap<>();
+        for (Map<String, String> item : list) {
+            String key = item.get("key");
+            String value = item.get("value");
+            if (key != null) {
+                result.put(key, value != null ? value : key);
+            }
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 获取TTS音色来源列表
+     */
+    @GetMapping("/voiceSourceList")
+    public AjaxResult getVoiceSourceList() {
+        List<Map<String, String>> list = inboundLlmMapper.selectVoiceSourceList();
+        // 转换为Map格式
+        Map<String, String> result = new HashMap<>();
+        for (Map<String, String> item : list) {
+            String key = item.get("key");
+            String value = item.get("value");
+            if (key != null) {
+                result.put(key, value != null ? value : key);
+            }
+        }
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 根据音色来源获取音色列表
+     */
+    @GetMapping("/voiceList")
+    public AjaxResult getVoiceList(@RequestParam("voiceSource") String voiceSource) {
+        List<EasyCallVoiceCodeVO> list = inboundLlmMapper.selectVoiceListBySource(voiceSource);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取业务组列表
+     */
+    @GetMapping("/bizGroupList")
+    public AjaxResult getBizGroupList() {
+        List<EasyCallBizGroupVO> list = inboundLlmMapper.selectBizGroupList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取出局网关列表
+     */
+    @GetMapping("/gatewayList")
+    public AjaxResult getGatewayList() {
+        List<EasyCallGatewayVO> list = inboundLlmMapper.selectOutboundGatewayList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取IVR列表
+     */
+    @GetMapping("/ivrList")
+    public AjaxResult getIvrList() {
+        List<EasyCallIvrVO> list = inboundLlmMapper.selectIvrList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 查询呼入通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('inboundCallManage:inboundCallRecord:list')")
+    @GetMapping("/inboundCdrList")
+    public TableDataInfo inboundCdrList(EasyCallInboundCdrVO vo) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+
+        // 通过CompanyInboundBind查询该公司绑定的呼入配置
+        List<CompanyInboundBind> binds = companyInboundBindMapper.selectIdsByCompanyId(companyId);
+        if (binds == null || binds.isEmpty()) {
+            return getDataTable(new ArrayList<>());
+        }
+
+        // 获取绑定配置的ID列表
+        List<Long> llmAccountIds = binds.stream()
+                .map(CompanyInboundBind::getInboundLlmAccountId)
+                .collect(Collectors.toList());
+
+        // 通过llmAccountIds查询对应的callee列表
+        EasyCallInboundLlmVO query = new EasyCallInboundLlmVO();
+        query.setVisibleIds(llmAccountIds);
+        List<EasyCallInboundLlmVO> llmConfigs = inboundCallManageService.selectInboundLlmList(query);
+        List<String> calleeList = llmConfigs.stream()
+                .map(EasyCallInboundLlmVO::getCallee)
+                .filter(StringUtils::isNotEmpty)
+                .distinct()
+                .collect(Collectors.toList());
+
+        if (calleeList.isEmpty()) {
+            return getDataTable(new ArrayList<>());
+        }
+
+        // 设置visibleCallees用于数据隔离
+        vo.setVisibleCallees(calleeList);
+
+        // 分页查询
+        startPage();
+        List<EasyCallInboundCdrVO> list = inboundCallManageService.selectInboundCdrList(vo);
+        TableDataInfo rspData = getDataTable(list);
+
+        // 处理录音URL
+        @SuppressWarnings("unchecked")
+        List<EasyCallInboundCdrVO> records = (List<EasyCallInboundCdrVO>) rspData.getRows();
+        for (EasyCallInboundCdrVO cdr : records) {
+            if (StringUtils.isNotEmpty(cdr.getWavFile())) {
+                cdr.setWavFileUrl("http://129.28.164.235:8899/recordings/files?filename=" + cdr.getWavFile());
+            }
+        }
+        rspData.setRows(records);
+        return rspData;
+    }
+
+    /**
+     * 填充关联数据
+     */
+    private void fillRelationData(EasyCallInboundLlmVO data) {
+        data.setAiTransferGroupId(data.getAiTransferData());
+        // 填充大模型账户名称
+        if (data.getLlmAccountId() != null && data.getLlmAccountId() > 0) {
+            EasyCallLlmAccountVO llmAccount = inboundLlmMapper.selectLlmAccountById(data.getLlmAccountId());
+            if (llmAccount != null) {
+                data.setLlmAccountName(llmAccount.getName());
+            } else {
+                data.setLlmAccountName("");
+            }
+        }
+        // 填充音色名称
+        if (StringUtils.isNotEmpty(data.getVoiceCode())) {
+            EasyCallVoiceCodeVO voiceCode = inboundLlmMapper.selectVoiceCodeByCode(data.getVoiceCode());
+            if (voiceCode != null) {
+                data.setVoiceSource(voiceCode.getVoiceSource());
+                data.setVoiceName(voiceCode.getVoiceName());
+            }
+        }
+    }
+}

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

@@ -4,7 +4,10 @@ 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.common.utils.ServletUtils;
 import com.fs.company.service.ICompanyVoiceCloneService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -23,6 +26,8 @@ public class CompanyVoiceCloneController extends BaseController {
 
     @Autowired
     private ICompanyVoiceCloneService companyVoiceCloneService;
+    @Autowired
+    private TokenService tokenService;
 
     /**
      * 上传音频文件并训练声音克隆音色
@@ -41,7 +46,10 @@ public class CompanyVoiceCloneController extends BaseController {
             @RequestParam("speaker_id") String speakerId,
             @RequestParam(value = "language", defaultValue = "0") Integer language,
             @RequestParam(value = "model_type", defaultValue = "2") Integer modelType) {
-        return companyVoiceCloneService.uploadAndTrain(voiceName, speakerId, language, modelType, file);
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return companyVoiceCloneService.uploadAndTrain(voiceName, speakerId, language, modelType, file,loginUser.getCompany().getCompanyId(),
+                loginUser.getUser().getUserId());
     }
 
     /**

+ 35 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java

@@ -11,6 +11,7 @@ import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogAddwxService;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddWxExportVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -115,6 +116,40 @@ public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
         return toAjax(companyVoiceRoboticCallLogAddwxService.deleteCompanyVoiceRoboticCallLogAddwxByLogIds(logIds));
     }
 
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/listByClientIdAndRoboticId")
+    public TableDataInfo listByClientIdAndRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx) {
+        startPage();
+        List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listByRoboticId(companyVoiceRoboticCallLogAddwx);
+        return getDataTable(list);
+
+    }
+
+
+    /**
+     * 加微统计数据(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/groupList")
+    public TableDataInfo groupList(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallLogAddwx> list = companyVoiceRoboticCallLogAddwxService.selectCompanyVoiceRoboticAddwxLogGroupList(companyVoiceRoboticCallLogAddwx);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询调用日志_发送短信列表统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/count")
+    public AjaxResult selectCompanyVoiceRoboticAddwxLogCount()
+    {
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogAddwxService.selectCompanyVoiceRoboticAddwxLogCount();
+        return AjaxResult.success(companyVoiceRoboticCallLogCount);
+    }
+
+
 //    /**
 //     * 导出调用日志_加微信列表
 //     */

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

@@ -9,6 +9,7 @@ import com.fs.aicall.domain.result.EditDialogResult;
 import com.fs.aicall.domain.result.GetairobotResult;
 import com.fs.aicall.domain.result.QueryCallTaskInfoResult;
 import com.fs.aicall.service.AiCallService;
+import com.fs.common.annotation.CallbackIpCheck;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -21,12 +22,16 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
 import com.fs.company.domain.CompanyVoiceRoboticWx;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.service.impl.CompanyUserServiceImpl;
 import com.fs.company.vo.CdrBodyVo;
 import com.fs.company.vo.CdrDetailVo;
+import com.fs.company.vo.DocCompanyUserVO;
 import com.fs.company.vo.WorkflowExecRecordVo;
+import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import lombok.extern.slf4j.Slf4j;
@@ -35,8 +40,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Arrays;
-import java.util.List;
+import java.util.*;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -60,6 +65,13 @@ public class CompanyVoiceRoboticController extends BaseController
     private ICompanyVoiceRoboticWxService companyVoiceRoboticWxService;
     @Autowired
     private TokenService tokenService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
+    @Autowired
+    private CompanyUserServiceImpl companyUserService;
+
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
 
     /**
      * 查询机器人外呼任务列表
@@ -71,6 +83,7 @@ public class CompanyVoiceRoboticController extends BaseController
         companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+        fillUserInfo(list);
         return getDataTable(list);
     }
     /**
@@ -231,6 +244,7 @@ public class CompanyVoiceRoboticController extends BaseController
     }
 
     @PostMapping("/callerResult4EasyCall")
+    @CallbackIpCheck
     public String callerResult4EasyCall(@RequestBody String cdrStr) {
         log.info("callerResult4EasyCall:回调结果:{}",cdrStr);
         CdrDetailVo cdrDetailVo = JSONObject.parseObject(cdrStr, CdrDetailVo.class);
@@ -305,4 +319,36 @@ 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());
+                }
+            }
+        });
+    }
 }

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

@@ -1,11 +1,13 @@
 package com.fs.company.controller.company;
 
 import com.fs.aicall.domain.CcLlmAgentAccount;
+import com.fs.aicall.domain.CcTtsAliyun;
 import com.fs.aicall.service.ICcLlmAgentAccountService;
 import com.fs.aicall.service.ICompanyBindAiModelService;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.ServletUtils;
+import com.fs.company.mapper.CompanyVoiceCloneRefMapper;
 import com.fs.company.service.easycall.IEasyCallService;
 import com.fs.company.vo.easycall.*;
 import com.fs.framework.security.LoginUser;
@@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * @author MixLiu
@@ -45,6 +48,9 @@ public class EasyCallController extends BaseController {
     @Autowired
     private ICcLlmAgentAccountService ccLlmAgentAccountService;
 
+    @Autowired
+    private CompanyVoiceCloneRefMapper companyVoiceCloneRefMapper;
+
     // =================== 基础数据查询 ===================
 
     /**
@@ -93,8 +99,24 @@ public class EasyCallController extends BaseController {
     public R getVoiceCodeList() {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getUser().getCompanyId();
-        List<EasyCallVoiceCodeVO> list = easyCallService.getVoiceCodeList(companyId);
-        return R.ok().put("data", list);
+
+        List<Long> ttsIds = companyVoiceCloneRefMapper.selectByCompanyIdAndCompanyUserId(companyId, loginUser.getCompany().getUserId());
+        List<CcTtsAliyun> ccTtsAliyuns = companyVoiceCloneRefMapper.selectCcTtsAliyunList();
+
+        List<EasyCallVoiceCodeVO> result = ccTtsAliyuns.stream()
+                .filter(item ->
+                        item.getPriority() == 1 || (item.getPriority() == 0 && ttsIds.contains(item.getId()))
+                )
+                .map(item -> {
+                    EasyCallVoiceCodeVO vo = new EasyCallVoiceCodeVO();
+                    vo.setVoiceCode(item.getVoiceCode());
+                    vo.setVoiceName(item.getVoiceName());
+                    vo.setVoiceSource(item.getVoiceSource());
+                    return vo;
+                })
+                .collect(Collectors.toList());
+//        List<EasyCallVoiceCodeVO> list = easyCallService.getVoiceCodeList(companyId);
+        return R.ok().put("data", result);
     }
 
     /**

+ 16 - 4
fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java

@@ -1,8 +1,11 @@
 package com.fs.company.controller.company;
 
+import com.fs.common.annotation.CallbackIpCheck;
 import com.fs.common.core.domain.R;
 import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.param.InboundCallbackParam;
 import com.fs.company.service.IGeneralCustomerEntryService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -16,15 +19,24 @@ import org.springframework.web.bind.annotation.RestController;
  */
 @RestController
 @RequestMapping("/company/general/customer")
+@Slf4j
 public class GeneralCustomerEntryController {
 
     @Autowired
     IGeneralCustomerEntryService iGeneralCustomerEntryService;
 
-    @PostMapping("/entryCustomer")
-    public R entryCustomer(@RequestBody EntryCustomerParam param){
-       iGeneralCustomerEntryService.entryCustomer(param);
-       return R.ok("success");
+//    @PostMapping("/entryCustomer")
+//    public R entryCustomer(@RequestBody EntryCustomerParam param){
+//       iGeneralCustomerEntryService.entryCustomer(param);
+//       return R.ok("success");
+//    }
+
+    @PostMapping("/inboundCallback")
+    @CallbackIpCheck
+    public String inboundCallback(@RequestBody InboundCallbackParam param){
+        log.info("呼入回调:{}", param);
+        iGeneralCustomerEntryService.inboundCallback(param);
+        return "success";
     }
 
 

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

@@ -534,7 +534,7 @@ public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutbou
         companyVoiceRoboticCallLogCallphone.setCallTime(Long.valueOf(callPhoneRes.getTimeLen()));
         companyVoiceRoboticCallLogCallphone.setCallType(Integer.valueOf(callType));
 
-        String json = configService.selectConfigByKey("cid.config");
+        String json = configService.selectConfigByKey("cId.config");
         CidConfigVO cidConfigVO = JSONUtil.toBean(json, CidConfigVO.class);
         BigDecimal callCharge = cidConfigVO.getCallCharge();
         //

+ 30 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyInboundBind.java

@@ -0,0 +1,30 @@
+package com.fs.company.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 呼入线路模型绑定关系对象 company_inbound_bind
+ *
+ * @author fs
+ * @date 2026-04-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyInboundBind extends BaseEntity{
+
+    /** id */
+    private Long id;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 呼入线路模型id */
+    @Excel(name = "呼入线路模型id")
+    private Long inboundLlmAccountId;
+
+
+}

+ 42 - 0
fs-service/src/main/java/com/fs/company/domain/CompanySiptaskInfo.java

@@ -0,0 +1,42 @@
+package com.fs.company.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 任务与外呼sip任务关联关系对象 company_siptask_info
+ *
+ * @author fs
+ * @date 2026-04-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanySiptaskInfo extends BaseEntity{
+
+    /** 主键id */
+    private Long id;
+
+    /** 工作流id */
+    @Excel(name = "工作流id")
+    private Long workflowId;
+
+    /** 任务id */
+    @Excel(name = "任务id")
+    private Long taskId;
+
+    /** 节点key */
+    @Excel(name = "节点key")
+    private String nodeKey;
+
+    /** 对应sip任务id */
+    @Excel(name = "对应sip任务id")
+    private Long batchId;
+
+    /** sip外呼任务 */
+    @Excel(name = "sip外呼任务")
+    private String taskJson;
+
+
+}

+ 42 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceCloneRef.java

@@ -0,0 +1,42 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class CompanyVoiceCloneRef {
+
+    private Long id;
+
+    private String voiceName;
+
+    private String voiceCode;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 公司ID */
+    private Long companyUserId;
+
+    /** 音色ID,对应 cc_tts_aliyun.id */
+    private Integer ttsId;
+
+    /** 状态 */
+    private Integer status;
+
+    /** 创建人 */
+    private String createBy;
+
+    /** 创建时间 */
+    private Date createTime;
+
+    /** 更新人 */
+    private String updateBy;
+
+    /** 更新时间 */
+    private Date updateTime;
+
+    /** 备注 */
+    private String remark;
+}

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

@@ -138,4 +138,10 @@ public class CompanyVoiceRobotic {
 
     /** 删除标志 0正常 1删除 */
     private Integer delFlag;
+
+    @TableField(exist = false)
+    private String createByName;
+
+    @TableField(exist = false)
+    private String createByDeptName;
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWxAccount.java

@@ -101,4 +101,9 @@ public class CompanyWxAccount extends BaseEntity
 
     @TableField(exist = false)
     private String companyUserName;
+
+    /**
+     * 微信备注(唯一)
+     */
+    private String wxRemark;
 }

+ 111 - 0
fs-service/src/main/java/com/fs/company/domain/EasyCallInboundCdrVO.java

@@ -0,0 +1,111 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 呼入通话记录对象 cc_inbound_cdr
+ *
+ * @author fs
+ */
+@Data
+@Accessors(chain = true)
+public class EasyCallInboundCdrVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private String id;
+
+    /** 主叫号码 */
+    private String caller;
+
+    /** 被叫号码 */
+    private String callee;
+
+    /** 呼入时间 */
+    private Long inboundTime;
+
+    /** 请求转接的坐席组 */
+    private String groupId;
+
+    /** 电话被接听时间 */
+    private Long answeredTime;
+
+    /** 接听电话的分机号码 */
+    private String extnum;
+
+    /** 接听电话的员工工号 */
+    private String opnum;
+
+    /** 挂机时间 */
+    private Long hangupTime;
+
+    /** 服务时长 */
+    private Long answeredTimeLen;
+
+    /** 通话总时长 */
+    private Long timeLen;
+
+    /** 通话uuid */
+    private String uuid;
+
+    /** 录音文件名 */
+    private String wavFile;
+
+    /** 录音文件url访问地址 */
+    private String wavFileUrl;
+
+    /** AI客服对话内容 */
+    private String chatContent;
+
+    /** asr时长(秒) */
+    private Integer asrSeconds;
+
+    /** tts调用次数(次) */
+    private Integer ttsTimes;
+
+    /** 大模型tts的字符数(字符) */
+    private Integer ttsFlowTokens;
+
+    /** 总输入token数 */
+    private Integer inputTokens;
+
+    /** 总输出token数 */
+    private Integer outputTokens;
+
+    /** 总调用费用(asr+tts+大模型) */
+    private BigDecimal totalCost;
+
+    /** 计费状态(1:已计费、0:未计费) */
+    private Integer billingStatus;
+
+    /** 整个ivr通话中的有效按键 */
+    private String ivrDtmfDigits;
+
+    /** 挂机原因 */
+    private String hangupCause;
+
+    /** 人工接听时间 */
+    private String manualAnsweredTime;
+
+    /** 人工接听时长 */
+    private String manualAnsweredTimeLen;
+
+    /************ 以下不是表结构字段 ************/
+
+    /** 业务组名称 */
+    private String groupName;
+
+    /** 请求参数(时间范围、时长范围等) */
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    private Map<String, Object> params;
+
+    /** 可见被叫号码列表(多租户隔离用) */
+    private List<String> visibleCallees;
+}

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

@@ -3,6 +3,7 @@ package com.fs.company.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyAiWorkflowExec;
 import com.fs.company.vo.WorkflowExecRecordVo;
+import com.fs.wxcid.domain.WxContact;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
@@ -110,4 +111,8 @@ public interface CompanyAiWorkflowExecMapper extends BaseMapper<CompanyAiWorkflo
             @Param("customerPhone") String customerPhone,
             @Param(("onlyCallNode")) Boolean onlyCallNode
     );
+
+    WxContact selectWxContectByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
+
+    Long selectWxClientIdByWorkflowInstanceId(@Param("workflowInstanceId") String workflowInstanceId);
 }

+ 67 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyInboundBindMapper.java

@@ -0,0 +1,67 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyInboundBind;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 呼入线路模型绑定关系Mapper接口
+ * 
+ * @author fs
+ * @date 2026-04-27
+ */
+public interface CompanyInboundBindMapper extends BaseMapper<CompanyInboundBind>{
+    /**
+     * 查询呼入线路模型绑定关系
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 呼入线路模型绑定关系
+     */
+    CompanyInboundBind selectCompanyInboundBindById(Long id);
+
+    /**
+     * 查询呼入线路模型绑定关系列表
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 呼入线路模型绑定关系集合
+     */
+    List<CompanyInboundBind> selectCompanyInboundBindList(CompanyInboundBind companyInboundBind);
+
+    /**
+     * 新增呼入线路模型绑定关系
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 结果
+     */
+    int insertCompanyInboundBind(CompanyInboundBind companyInboundBind);
+
+    /**
+     * 修改呼入线路模型绑定关系
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 结果
+     */
+    int updateCompanyInboundBind(CompanyInboundBind companyInboundBind);
+
+    /**
+     * 删除呼入线路模型绑定关系
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 结果
+     */
+    int deleteCompanyInboundBindById(Long id);
+
+    /**
+     * 批量删除呼入线路模型绑定关系
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanyInboundBindByIds(Long[] ids);
+
+    List<CompanyInboundBind> selectIdsByCompanyId(@Param("companyId") Long companyId);
+
+    int deleteByCompanyIdAndInboundLlmAccountIds(@Param("companyId") Long companyId, @Param("inboundLlmAccountIds") List<Long> inboundLlmAccountIds);
+}

+ 65 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanySiptaskInfoMapper.java

@@ -0,0 +1,65 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanySiptaskInfo;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 任务与外呼sip任务关联关系Mapper接口
+ * 
+ * @author fs
+ * @date 2026-04-20
+ */
+public interface CompanySiptaskInfoMapper extends BaseMapper<CompanySiptaskInfo>{
+    /**
+     * 查询任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 任务与外呼sip任务关联关系
+     */
+    CompanySiptaskInfo selectCompanySiptaskInfoById(Long id);
+
+    /**
+     * 查询任务与外呼sip任务关联关系列表
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 任务与外呼sip任务关联关系集合
+     */
+    List<CompanySiptaskInfo> selectCompanySiptaskInfoList(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 新增任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int insertCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 修改任务与外呼sip任务关联关系
+     * 
+     * @param companySiptaskInfo 任务与外呼sip任务关联关系
+     * @return 结果
+     */
+    int updateCompanySiptaskInfo(CompanySiptaskInfo companySiptaskInfo);
+
+    /**
+     * 删除任务与外呼sip任务关联关系
+     * 
+     * @param id 任务与外呼sip任务关联关系主键
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoById(Long id);
+
+    /**
+     * 批量删除任务与外呼sip任务关联关系
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanySiptaskInfoByIds(Long[] ids);
+
+    CompanySiptaskInfo selectSipTaskInfoByTaskIdAndNodeKey(@Param("taskId") Long taskId, @Param("nodeKey") String nodeKey);
+}

+ 33 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceCloneRefMapper.java

@@ -0,0 +1,33 @@
+package com.fs.company.mapper;
+
+import com.fs.aicall.domain.CcTtsAliyun;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.CompanyVoiceCloneRef;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyVoiceCloneRefMapper {
+
+    CompanyVoiceCloneRef selectByCompanyIdAndTtsId(@Param("companyId") Long companyId,
+                                                   @Param("companyUserId") Long companyUserId,
+                                                   @Param("ttsId") Integer ttsId);
+
+    int insertCompanyVoiceCloneRef(CompanyVoiceCloneRef ref);
+
+    int updateCompanyVoiceCloneRef(CompanyVoiceCloneRef ref);
+
+    List<Long> selectByCompanyIdAndCompanyUserId(@Param("companyId") Long companyId,
+                                   @Param("companyUserId") Long companyUserId);
+
+
+    @DataSource(DataSourceType.EASYCALL)
+    List<CcTtsAliyun> selectCcTtsAliyunList();
+
+
+
+
+
+
+}

+ 20 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogAddwxMapper.java

@@ -3,6 +3,7 @@ package com.fs.company.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 
 import java.util.List;
 import java.util.Map;
@@ -69,5 +70,24 @@ public interface CompanyVoiceRoboticCallLogAddwxMapper extends BaseMapper<Compan
     List<CompanyVoiceRoboticCallLogAddwxVO> listAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
 
     Map<String, Long> countListAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+    /**
+     * 查询加微记录分组列表
+     * @param companyVoiceRoboticCallLogAddwx
+     * @return
+     */
+    List<CompanyVoiceRoboticCallLogAddwx> selectCompanyVoiceRoboticCallLogAddwxGroupList (CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    /**
+     * 查询加微记录详情
+     * @param companyVoiceRoboticCallLogAddwx
+     * @return
+     */
+    List<CompanyVoiceRoboticCallLogAddwxVO> listByRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    /**
+     * 查询加微记录统计(所有任务)
+     * @return
+     */
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticAddwxLogCount();
 
 }

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

@@ -67,4 +67,6 @@ public interface CompanyWxAccountMapper extends BaseMapper<CompanyWxAccount> {
     List<CompanyUser> companyListAllCompany(CompanyUser companyUser);
 
     CompanyWxAccount selectByCompanyUserAndWxNo(@Param("userId") Long userId, @Param("wxNo") String wxNo);
+
+    CompanyWxAccount selectCompanyWxAccountByWxRemark(@Param("wxRemark") String wxRemark);
 }

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

@@ -85,4 +85,6 @@ public interface CompanyWxClientMapper extends BaseMapper<CompanyWxClient> {
     List<CompanyWxClient> getQwAddWxList(@Param("accountIdList") List<Long> accountIdList, @Param("isWeCom") Integer isWeCom);
 
     List<CompanyWxClient4WorkFlowVO> getQwAddWxList4Workflow(@Param("accountIdList") List<Long> accountIdList, @Param("execStatus") Integer execStatus, @Param("execNodeType") Integer execNodeType, @Param("cidGroupNo") Integer cidGroupNo);
+
+    List<CompanyWxClient> selectWxV2(@Param("id") Long id, @Param("phone") String phone);
 }

+ 167 - 0
fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java

@@ -0,0 +1,167 @@
+package com.fs.company.mapper;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.EasyCallInboundCdrVO;
+import com.fs.company.vo.easycall.*;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 呼入大模型配置 Mapper接口
+ *
+ * @author fs
+ */
+@Repository
+public interface EasyCallInboundLlmMapper {
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallInboundLlmVO selectInboundLlmById(Integer id);
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo);
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int insertInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int updateInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteInboundLlmById(Integer id);
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID数组
+     * @return 影响行数
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteInboundLlmByIds(String[] ids);
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee);
+
+    /**
+     * 查询大模型账户列表
+     *
+     * @return 大模型账户列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallLlmAccountVO> selectLlmAccountList(@Param("modelsId") List<Long> modelsId);
+
+    /**
+     * 根据ID查询大模型账户
+     *
+     * @param id 大模型账户ID
+     * @return 大模型账户信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallLlmAccountVO selectLlmAccountById(Integer id);
+
+    /**
+     * 根据voiceCode查询音色信息
+     *
+     * @param voiceCode 音色编号
+     * @return 音色信息
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    EasyCallVoiceCodeVO selectVoiceCodeByCode(String voiceCode);
+
+    /**
+     * 查询ASR提供商列表
+     *
+     * @return ASR提供商列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<Map<String, String>> selectAsrProviderList();
+
+    /**
+     * 查询TTS音色来源列表
+     *
+     * @return TTS音色来源列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<Map<String, String>> selectVoiceSourceList();
+
+    /**
+     * 根据音色来源查询音色列表
+     *
+     * @param voiceSource 音色来源
+     * @return 音色列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallVoiceCodeVO> selectVoiceListBySource(String voiceSource);
+
+    /**
+     * 查询业务组列表
+     *
+     * @return 业务组列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallBizGroupVO> selectBizGroupList();
+
+    /**
+     * 查询出局网关列表
+     *
+     * @return 网关列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallGatewayVO> selectOutboundGatewayList();
+
+    /**
+     * 查询IVR列表
+     *
+     * @return IVR列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallIvrVO> selectIvrList();
+
+    /**
+     * 查询呼入通话记录列表
+     *
+     * @param vo 查询条件
+     * @return 通话记录列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallInboundCdrVO> selectInboundCdrList(EasyCallInboundCdrVO vo);
+}

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

@@ -2,6 +2,7 @@ package com.fs.company.mapper;
 
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
+import com.fs.company.vo.InboundCallInfo;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.company.vo.easycall.EasyCallOutBoundVO;
 import org.apache.ibatis.annotations.Param;
@@ -22,4 +23,8 @@ public interface EasyCallMapper {
     @DataSource(DataSourceType.EASYCALL)
     EasyCallOutBoundVO getOutBoundInfoByUuid(@Param("uuid") String uuid);
 
+
+    @DataSource(DataSourceType.EASYCALL)
+    InboundCallInfo selectInboundCallbackInfoByUuid(@Param("uuid") String uuid);
+
 }

+ 21 - 0
fs-service/src/main/java/com/fs/company/param/AddWxActionParam.java

@@ -0,0 +1,21 @@
+package com.fs.company.param;
+
+import com.fs.wxcid.vo.wxvo.AddWxVo;
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/22 14:56
+ * @description
+ */
+
+@Data
+public class AddWxActionParam extends AddWxVo {
+
+    private String wxId;
+
+    private Long serverId;
+
+
+
+}

+ 19 - 0
fs-service/src/main/java/com/fs/company/param/InboundCallbackParam.java

@@ -0,0 +1,19 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/28 09:59
+ * @description
+ */
+
+@Data
+public class InboundCallbackParam {
+
+    /**
+     * 数据uuid
+     */
+    private String uuid;
+
+}

+ 6 - 0
fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java

@@ -62,4 +62,10 @@ public interface CompanyWorkflowEngine {
      * @param inputData
      */
     void timeDoExecute(String workflowInstanceId, String nodeKey, Map<String, Object> inputData);
+    /**
+     * 创建sip任务
+     * @param roboticId
+     * @param workFlowId
+     */
+    Long createSipTask(Long roboticId,Long workFlowId);
 }

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

@@ -0,0 +1,78 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.EasyCallInboundCdrVO;
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+
+import java.util.List;
+
+/**
+ * 呼入大模型配置 Service接口
+ *
+ * @author fs
+ */
+public interface ICompanyInboundCallManageService {
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    EasyCallInboundLlmVO selectInboundLlmById(Integer id);
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo);
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee);
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    int insertInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    int updateInboundLlm(EasyCallInboundLlmVO vo);
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    int deleteInboundLlmById(Integer id);
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID字符串,逗号分隔
+     * @return 影响行数
+     */
+    int deleteInboundLlmByIds(String ids,Long companyId);
+
+    /**
+     * 查询呼入通话记录列表
+     *
+     * @param vo 查询条件
+     * @return 通话记录列表
+     */
+    List<EasyCallInboundCdrVO> selectInboundCdrList(EasyCallInboundCdrVO vo);
+}

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

@@ -0,0 +1,15 @@
+package com.fs.company.service;
+
+import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
+
+import java.util.List;
+
+/**
+ * 豆包声音克隆 Service 接口
+ *
+ * @author fs
+ */
+public interface ICompanyVoiceCloneRefService {
+
+    List<EasyCallVoiceCodeVO> getVoiceCodeList(Long companyId);
+}

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

@@ -22,7 +22,7 @@ public interface ICompanyVoiceCloneService {
      */
     AjaxResult uploadAndTrain(String voiceName, String speakerId,
                               Integer language, Integer modelType,
-                              MultipartFile file);
+                              MultipartFile file,Long companyId,Long companyUserId);
 
     /**
      * TTS 语音合成测试

+ 13 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogAddwxService.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
 import com.fs.company.domain.CompanyWxClient;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 
 import java.util.List;
 import java.util.Map;
@@ -76,4 +77,16 @@ public interface ICompanyVoiceRoboticCallLogAddwxService extends IService<Compan
 
     Map<String, Long> countListAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
 
+    List<CompanyVoiceRoboticCallLogAddwx> selectCompanyVoiceRoboticAddwxLogGroupList(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticAddwxLogCount();
+
+    /**
+     * 查询加微记录详情
+     * @param companyVoiceRoboticCallLogAddwx
+     * @return
+     */
+    List<CompanyVoiceRoboticCallLogAddwxVO> listByRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx);
+
+
 }

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

@@ -1,6 +1,7 @@
 package com.fs.company.service;
 
 import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.param.InboundCallbackParam;
 
 /**
  * @author MixLiu
@@ -12,4 +13,6 @@ public interface IGeneralCustomerEntryService {
 //    R entryCustomer(String param);
 
     void entryCustomer(EntryCustomerParam param);
+
+    void inboundCallback(InboundCallbackParam param);
 }

+ 13 - 0
fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java

@@ -8,6 +8,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.vo.easycall.*;
+import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -31,6 +32,9 @@ import java.util.stream.Collectors;
 @Slf4j
 public class EasyCallServiceImpl implements IEasyCallService {
 
+    @Autowired
+    private ISysConfigService configService;
+
     /**
      * EasyCallCenter365 服务器基础地址,从配置文件 easycall.base-url 读取
      */
@@ -104,6 +108,15 @@ public class EasyCallServiceImpl implements IEasyCallService {
             if(StringUtils.isNotBlank(gateWayList)){
                 List<Long> collect = Arrays.stream(gateWayList.split(",")).map(item -> Long.valueOf(item.trim())).collect(Collectors.toList());
                 resList = resList.stream().filter(item -> collect.contains(item.getId())).collect(Collectors.toList());
+            }else{
+                String json = configService.selectConfigByKey("cId.config");
+                if (StringUtils.isNotBlank(json)) {
+                    JSONObject obj = JSONObject.parseObject(json);
+                    if(null != obj && obj.containsKey("showGatewayIds")){
+                        List<Long> showGatewayIds = obj.getJSONArray("showGatewayIds").stream().map(item -> Long.valueOf(item.toString())).collect(Collectors.toList());
+                        resList = resList.stream().filter(item -> showGatewayIds.contains(item.getId())).collect(Collectors.toList());
+                    }
+                }
             }
         }
         return resList;

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

@@ -127,7 +127,7 @@ public class AsyncCalleeProcessorServiceImpl implements IAsyncCalleeProcessorSer
 
     private CidPhoneConfig loadPhoneConfig(Long companyId) {
         try {
-            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(companyId, "cid.config");
+            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(companyId, "cId.config");
             if (companyConfig != null && StringUtils.isNotEmpty(companyConfig.getConfigValue())) {
                 return JSONObject.parseObject(companyConfig.getConfigValue(), CidPhoneConfig.class);
             }

+ 174 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java

@@ -0,0 +1,174 @@
+package com.fs.company.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.text.Convert;
+import com.fs.company.domain.CompanyInboundBind;
+import com.fs.company.domain.EasyCallInboundCdrVO;
+import com.fs.company.mapper.CompanyInboundBindMapper;
+import com.fs.company.mapper.EasyCallInboundLlmMapper;
+import com.fs.company.service.ICompanyInboundCallManageService;
+import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 呼入大模型配置 Service业务层处理
+ *
+ * @author fs
+ */
+@Service
+@Slf4j
+public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallManageService {
+
+    @Autowired
+    private EasyCallInboundLlmMapper inboundLlmMapper;
+
+    @Autowired
+    CompanyInboundBindMapper companyInboundBindMapper;
+    @Autowired
+    private ISysConfigService sysConfigService;
+
+    /**
+     * 查询呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 配置信息
+     */
+    @Override
+    public EasyCallInboundLlmVO selectInboundLlmById(Integer id) {
+        return inboundLlmMapper.selectInboundLlmById(id);
+    }
+
+    /**
+     * 查询呼入大模型配置列表
+     *
+     * @param vo 查询条件
+     * @return 配置列表
+     */
+    @Override
+    public List<EasyCallInboundLlmVO> selectInboundLlmList(EasyCallInboundLlmVO vo) {
+        return inboundLlmMapper.selectInboundLlmList(vo);
+    }
+
+    /**
+     * 根据被叫号码查询配置列表
+     *
+     * @param callee 被叫号码
+     * @return 配置列表
+     */
+    @Override
+    public List<EasyCallInboundLlmVO> selectInboundLlmByCallee(String callee) {
+        return inboundLlmMapper.selectInboundLlmByCallee(callee);
+    }
+
+    /**
+     * 新增呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @Override
+    public int insertInboundLlm(EasyCallInboundLlmVO vo) {
+        Boolean b = checkCalleeInboundLlm(vo.getCallee());
+        if(b){
+          throw new RuntimeException("被叫号码已存在,不能重复插入");
+        }
+        try {
+            //获得总后台配置
+            SysConfig cidConf = sysConfigService.selectConfigByConfigKey("cId.config");
+            if (null != cidConf) {
+                String configValue = cidConf.getConfigValue();
+                if (com.fs.common.utils.StringUtils.isNotBlank(configValue)) {
+                    JSONObject jsonObject = JSONObject.parseObject(configValue);
+                    if (null != jsonObject && jsonObject.containsKey("inboundCallbackUrl")) {
+                        vo.setCallBackUrl(jsonObject.getString("inboundCallbackUrl"));
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.error("获取总后台配置异常", ex);
+        }
+        
+        int i = inboundLlmMapper.insertInboundLlm(vo);
+        if(i >0 && vo.getId()!= null) {
+            CompanyInboundBind bind = new CompanyInboundBind();
+            bind.setInboundLlmAccountId(Long.valueOf(vo.getId()));
+            bind.setCompanyId(vo.getCompanyId());
+            bind.setCreateTime(new Date());
+            companyInboundBindMapper.insertCompanyInboundBind( bind);
+        }
+        return i;
+    }
+
+    /**
+     * 修改呼入大模型配置
+     *
+     * @param vo 配置信息
+     * @return 影响行数
+     */
+    @Override
+    public int updateInboundLlm(EasyCallInboundLlmVO vo) {
+        List<EasyCallInboundLlmVO> list = inboundLlmMapper.selectInboundLlmByCallee(vo.getCallee());
+        if(list != null && list.size() > 0 && !list.get(0).getId().equals(vo.getId())){
+            throw new RuntimeException("被叫号码已存在,不能重复");
+        }
+        return inboundLlmMapper.updateInboundLlm(vo);
+    }
+
+    /**
+     * 删除呼入大模型配置
+     *
+     * @param id 主键ID
+     * @return 影响行数
+     */
+    @Override
+    public int deleteInboundLlmById(Integer id) {
+        return inboundLlmMapper.deleteInboundLlmById(id);
+    }
+
+    /**
+     * 批量删除呼入大模型配置
+     *
+     * @param ids ID字符串,逗号分隔
+     * @return 影响行数
+     */
+    @Override
+    public int deleteInboundLlmByIds(String ids,Long companyId) {
+        int i = inboundLlmMapper.deleteInboundLlmByIds(Convert.toStrArray(ids));
+        if(StringUtils.isNotBlank(ids) && null != companyId ){
+            List<Long> collect = Arrays.stream(ids.split(",")).map(id -> Long.valueOf(id)).collect(Collectors.toList());
+            companyInboundBindMapper.deleteByCompanyIdAndInboundLlmAccountIds(companyId,collect );
+        }
+        return i;
+    }
+
+    /**
+     * 查询呼入通话记录列表
+     *
+     * @param vo 查询条件
+     * @return 通话记录列表
+     */
+    @Override
+    public List<EasyCallInboundCdrVO> selectInboundCdrList(EasyCallInboundCdrVO vo) {
+        return inboundLlmMapper.selectInboundCdrList(vo);
+    }
+
+    /**
+     * 检查被叫号码是否在呼入大模型配置中
+     * @param callee
+     * @return
+     */
+    public Boolean checkCalleeInboundLlm(String callee) {
+        List<EasyCallInboundLlmVO> list = inboundLlmMapper.selectInboundLlmByCallee(callee);
+        return list != null && list.size() > 0;
+    }
+}

+ 37 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneRefServiceImpl.java

@@ -0,0 +1,37 @@
+package com.fs.company.service.impl;
+
+import com.fs.company.mapper.CompanyVoiceCloneRefMapper;
+import com.fs.company.service.ICompanyVoiceCloneRefService;
+import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 豆包声音克隆 Service 实现
+ * <p>
+ * 参照 DoubaoVclController 实现:
+ * 1. uploadAndTrain: 从 EASYCALL 读取账号,HTTP 调用豆包训练接口,成功后写入音色表
+ * 2. doubaoTtsTest: HTTP 调用豆包 TTS 接口,返回 base64 音频数据
+ * </p>
+ *
+ * @author fs
+ */
+@Service
+@Slf4j
+public class CompanyVoiceCloneRefServiceImpl implements ICompanyVoiceCloneRefService {
+
+    @Autowired
+    private CompanyVoiceCloneRefMapper companyVoiceCloneRefMapper;
+
+    @Override
+    public List<EasyCallVoiceCodeVO> getVoiceCodeList(Long companyId) {
+
+        return Collections.emptyList();
+    }
+
+
+}

+ 47 - 3
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneServiceImpl.java

@@ -8,6 +8,8 @@ import com.fs.aicall.service.ICcParamsService;
 import com.fs.aicall.service.ICcTtsAliyunService;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyVoiceCloneRef;
+import com.fs.company.mapper.CompanyVoiceCloneRefMapper;
 import com.fs.company.service.ICompanyVoiceCloneService;
 import lombok.extern.slf4j.Slf4j;
 import okhttp3.*;
@@ -45,6 +47,9 @@ public class CompanyVoiceCloneServiceImpl implements ICompanyVoiceCloneService {
 
     private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
 
+    @Autowired
+    private CompanyVoiceCloneRefMapper companyVoiceCloneRefMapper;
+
     /** 错误码映射 */
     private static final Map<Integer, String> ERROR_MAP = new HashMap<Integer, String>() {{
         put(1001, "BadRequestError: 请求参数有误");
@@ -84,7 +89,7 @@ public class CompanyVoiceCloneServiceImpl implements ICompanyVoiceCloneService {
     @Override
     public AjaxResult uploadAndTrain(String voiceName, String speakerId,
                                      Integer language, Integer modelType,
-                                     MultipartFile file) {
+                                     MultipartFile file,Long companyId,Long companyUserId) {
         if (file == null || file.isEmpty()) {
             return AjaxResult.error("请选择音频文件");
         }
@@ -127,7 +132,8 @@ public class CompanyVoiceCloneServiceImpl implements ICompanyVoiceCloneService {
 
                 if (statusCode == 0) {
                     // 训练成功,写入音色表
-                    addSpeakerId(voiceName, speakerId);
+                    CcTtsAliyun ttsAliyun = addSpeakerId(voiceName, speakerId);
+                    addCompanyVoiceRelation(ttsAliyun.getVoiceCode(),voiceName,companyId, ttsAliyun.getId(), companyUserId);
 
                     // 查询训练状态
                     boolean modelReady = getStatus(appid, token, speakerId);
@@ -260,7 +266,7 @@ public class CompanyVoiceCloneServiceImpl implements ICompanyVoiceCloneService {
      * @param nameParam
      * @param speakerId
      */
-    private synchronized void addSpeakerId(String nameParam, String speakerId) {
+    private synchronized CcTtsAliyun addSpeakerId(String nameParam, String speakerId) {
         CcTtsAliyun ttsSpeaker = ccTtsAliyunService.selectCcTtsAliyunByVoiceCode(speakerId);
         String name = nameParam.replace("'", "").replace(" ", "");
         if (name.length() > 20) {
@@ -287,9 +293,14 @@ public class CompanyVoiceCloneServiceImpl implements ICompanyVoiceCloneService {
             } else {
                 ccTtsAliyunService.insertCcTtsAliyun(ttsSpeaker);
             }
+
+            // 重新查一次,确保拿到最新id
+            CcTtsAliyun saved = ccTtsAliyunService.selectCcTtsAliyunByVoiceCode(speakerId);
             log.info("save doubaovcl speakerId succeed. {} {}", name, speakerId);
+            return saved;
         } catch (Exception e) {
             log.error("save doubaovcl speakerId error: {}", e.getMessage(), e);
+            throw new RuntimeException("保存音色失败", e);
         }
     }
 
@@ -388,4 +399,37 @@ public class CompanyVoiceCloneServiceImpl implements ICompanyVoiceCloneService {
                 .writeTimeout(60, TimeUnit.SECONDS)
                 .build();
     }
+
+    private synchronized void addCompanyVoiceRelation(String code,String name,Long companyId, Integer ttsId,Long companyUserId) {
+        if (companyId == null) {
+            throw new RuntimeException("companyId为空,无法保存公司音色关联");
+        }
+        if (ttsId == null) {
+            throw new RuntimeException("ttsId为空,无法保存公司音色关联");
+        }
+
+        try {
+            CompanyVoiceCloneRef ref = companyVoiceCloneRefMapper.selectByCompanyIdAndTtsId(companyId ,companyUserId, ttsId);
+            if (ref == null) {
+                ref = new CompanyVoiceCloneRef();
+                ref.setVoiceName(name);
+                ref.setVoiceCode(code);
+                ref.setCompanyId(companyId);
+                ref.setCompanyUserId(companyUserId);
+                ref.setTtsId(ttsId);
+                ref.setStatus(1);
+                ref.setCreateBy("system");
+                ref.setUpdateBy("system");
+                companyVoiceCloneRefMapper.insertCompanyVoiceCloneRef(ref);
+            } else {
+                ref.setStatus(1);
+                ref.setUpdateBy("system");
+                companyVoiceCloneRefMapper.updateCompanyVoiceCloneRef(ref);
+            }
+            log.info("保存公司音色关联成功,companyId={}, ttsId={}", companyId, ttsId);
+        } catch (Exception e) {
+            log.error("保存公司音色关联失败,companyId={}, ttsId={}", companyId, ttsId, e);
+            throw new RuntimeException("保存公司音色关联失败", e);
+        }
+    }
 }

+ 16 - 3
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogAddwxServiceImpl.java

@@ -9,6 +9,7 @@ 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.CompanyVoiceRoboticCallLogCount;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
@@ -25,9 +26,7 @@ import java.util.Map;
  */
 @Service
 @Slf4j
-public
-
-class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<CompanyVoiceRoboticCallLogAddwxMapper, CompanyVoiceRoboticCallLogAddwx> implements ICompanyVoiceRoboticCallLogAddwxService {
+public class CompanyVoiceRoboticCallLogAddwxServiceImpl extends ServiceImpl<CompanyVoiceRoboticCallLogAddwxMapper, CompanyVoiceRoboticCallLogAddwx> implements ICompanyVoiceRoboticCallLogAddwxService {
 
 
     @Autowired
@@ -160,5 +159,19 @@ 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);
+    }
+
+    @Override
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticAddwxLogCount() {
+        return baseMapper.selectCompanyVoiceRoboticAddwxLogCount();
+    }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogAddwxVO> listByRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx) {
+        return baseMapper.listByRoboticId(companyVoiceRoboticCallLogAddwx);
+    }
 
 }

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

@@ -304,7 +304,7 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
             }
             CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(calleeId);
             try {
-                String json = configService.selectConfigByKey("cid.config");
+                String json = configService.selectConfigByKey("cId.config");
                 if (StringUtils.isBlank(json)) {
                     log.error("未配置cid.config");
                 }

+ 133 - 16
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -34,6 +34,7 @@ import com.fs.core.config.TenantConfigContext;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.crm.param.SmsSendBatchParam;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
 import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
@@ -61,6 +62,7 @@ import org.springframework.stereotype.Service;
 
 import java.lang.reflect.Method;
 import java.text.SimpleDateFormat;
+import java.time.temporal.ChronoField;
 import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -68,6 +70,7 @@ import java.util.stream.Stream;
 
 import static com.fs.company.service.impl.call.node.AbstractWorkflowNode.companyVoiceRoboticCallLogCallphoneMapper;
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
+import static java.time.LocalTime.now;
 
 
 /**
@@ -133,12 +136,20 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     private final IAsyncCalleeProcessorService asyncCalleeProcessorService;
 
+    private final ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+
     /** EasyCall intent 意向度重试队列 Redis key 前缀,value 为已重试次数 */
     private static final String EASYCALL_INTENT_RETRY_KEY = "easycall:intent:retry:";
     /** intent 意向度等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
     private static final int EASYCALL_INTENT_MAX_RETRY = 5;
     /** 每次重试等待时长(毫秒) */
     private static final long EASYCALL_INTENT_RETRY_INTERVAL_MS = 30000L;
+    /** EasyCall dialogue 对话内容重试队列 Redis key 前缀,value 为已重试次数 */
+    private static final String EASYCALL_DIALOGUE_RETRY_KEY = "easycall:dialogue:retry:";
+    /** dialogue 对话内容等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
+    private static final int EASYCALL_DIALOGUE_MAX_RETRY = 5;
+    /** dialogue 每次重试等待时长(毫秒) */
+    private static final long EASYCALL_DIALOGUE_RETRY_INTERVAL_MS = 30000L;
 
     /**
      * 查询机器人外呼任务
@@ -217,6 +228,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             client.setRoboticId(companyVoiceRobotic.getId());
             client.setCustomerId(Long.parseLong(e));
             client.setIsWeCom(isWeCom);
+            client.setCreateTime(new Date());
             return client;
         }).collect(Collectors.toList());
         companyWxClientServiceImpl.saveBatch(clients);
@@ -845,7 +857,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     @Async("cidWorkFlowExecutor")
     public void callerResult4EasyCall(CdrDetailVo result) {
         try {
-            Thread.sleep(20000L);
+            Thread.sleep(5000L);
         } catch (InterruptedException e) {
             throw new RuntimeException(e);
         }
@@ -889,27 +901,49 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 } catch (Exception e) {
                     log.error("callerResult4EasyCall 切换租户数据源失败: tenantId={}", TenantHelper.getTenantId(), e);
                 }
-                // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
-                if (StringUtils.isBlank(callPhoneRes.getIntent())) {
-                    String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
+//                // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
+//                if (StringUtils.isBlank(callPhoneRes.getIntent())) {
+//                    String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
+//                    Integer retryCount = redisCache2.getCacheObject(retryKey);
+//                    if (retryCount == null) {
+//                        retryCount = 0;
+//                    }
+//                    if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
+//                        redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+//                        log.info("easyCall外呼回调intent意向度暂未评估完成,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+//                        doRetryCallerResult4EasyCall(result, retryCount + 1);
+//                    } else {
+//                        // 超过最大重试次数,以 intent 为空(意向未知)兜底继续处理
+//                        log.warn("easyCall外呼回调intent意向度在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
+//                        redisCache2.deleteObject(retryKey);
+//                        doHandleEasyCallResult(callPhoneRes);
+//                    }
+//                    return;
+//                }
+//                // intent 已有值,直接正常处理
+//                redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+//                doHandleEasyCallResult(callPhoneRes);
+                // dialogue(对话内容)由对方异步写入,回调时可能尚未赋值,进入延迟重试队列等待
+                if (isDialogueEmpty(callPhoneRes.getDialogue()) && !"未接通".equals(callPhoneRes.getIntent())) {
+                    String retryKey = EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid();
                     Integer retryCount = redisCache2.getCacheObject(retryKey);
                     if (retryCount == null) {
                         retryCount = 0;
                     }
-                    if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
+                    if (retryCount < EASYCALL_DIALOGUE_MAX_RETRY) {
                         redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
-                        log.info("easyCall外呼回调intent意向度暂未评估完成,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
-                        doRetryCallerResult4EasyCall(result, retryCount + 1);
+                        log.info("easyCall外呼回调dialogue对话内容暂未写入,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+                        doRetryDialogue4EasyCall(result, retryCount + 1);
                     } else {
-                        // 超过最大重试次数,以 intent 为空(意向未知)兜底继续处理
-                        log.warn("easyCall外呼回调intent意向度在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
+                        // 超过最大重试次数,以 dialogue 为空兜底继续处理
+                        log.warn("easyCall外呼回调dialogue对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", EASYCALL_DIALOGUE_MAX_RETRY, result.getUuid());
                         redisCache2.deleteObject(retryKey);
                         doHandleEasyCallResult(callPhoneRes);
                     }
                     return;
                 }
-                // intent 已有值,直接正常处理
-                redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+                // dialogue 已有值,直接正常处理
+                redisCache2.deleteObject(EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid());
                 doHandleEasyCallResult(callPhoneRes);
             } catch (Exception e) {
                 throw new RuntimeException(e);
@@ -933,7 +967,56 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         }
 
     }
+    private boolean isDialogueEmpty(String dialogue) {
+        if (StringUtils.isBlank(dialogue)) {
+            return true;
+        }
+        String trimmed = dialogue.trim();
+        return "[]".equals(trimmed);
+    }
 
+    /**
+     * 延迟重试处理 EasyCall 外呼回调(等待 dialogue 对话内容异步写入完成)
+     * 每次重试前等待 {@link #EASYCALL_DIALOGUE_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
+     */
+    @Async("cidWorkFlowExecutor")
+    public void doRetryDialogue4EasyCall(CdrDetailVo result, int currentRetry) {
+        try {
+            Thread.sleep(EASYCALL_DIALOGUE_RETRY_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("easyCall dialogue重试等待被中断, uuid={}", result.getUuid());
+            return;
+        }
+        log.info("easyCall dialogue重试第{}次开始, uuid={}", currentRetry, result.getUuid());
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(result.getUuid());
+        if (null == callPhoneRes) {
+            log.error("easyCall dialogue重试时仍未查询到外呼结果, uuid={}", result.getUuid());
+            return;
+        }
+        if (isDialogueEmpty(callPhoneRes.getDialogue()) && !"未接通".equals(callPhoneRes.getIntent())) {
+            // dialogue 仍为空,继续判断是否还有剩余重试次数
+            String retryKey = EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = currentRetry;
+            }
+            if (retryCount < EASYCALL_DIALOGUE_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall dialogue对话内容仍未写入,uuid={},第{}次继续延迟重试", result.getUuid(), retryCount + 1);
+                doRetryDialogue4EasyCall(result, retryCount + 1);
+            } else {
+                log.warn("easyCall dialogue对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", EASYCALL_DIALOGUE_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // dialogue 已写入完成,正常处理
+        log.info("easyCall dialogue重试第{}次成功获取到对话内容,uuid={}", currentRetry, result.getUuid());
+        redisCache2.deleteObject(EASYCALL_DIALOGUE_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
     /**
      * 延迟重试处理 EasyCall 外呼回调(等待 intent 意向度异步评估完成)
      * 每次重试前等待 {@link #EASYCALL_INTENT_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
@@ -984,8 +1067,16 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private void doHandleEasyCallResult(EasyCallCallPhoneVO callPhoneRes) {
         //等待数据信息
         JSONObject bizJson = JSONObject.parseObject(callPhoneRes.getBizJson());
-        
-        String cacheString = (String) redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
+
+        Object cacheObj = redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
+        String cacheString;
+        if (cacheObj instanceof String) {
+            cacheString = (String) cacheObj;
+        } else if (cacheObj instanceof JSONObject) {
+            cacheString = ((JSONObject) cacheObj).toJSONString();
+        } else {
+            cacheString = cacheObj == null ? null : JSONObject.toJSONString(cacheObj);
+        }
         if (StringUtils.isBlank(cacheString)) {
             log.error("easyCall外呼回调缓存信息缺失, uuid={}", callPhoneRes.getUuid());
             return;
@@ -1063,7 +1154,24 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 //                    log.error("pushDialogContent4EasyCall 切换租户数据源失败: tenantId={}", TenantHelper.getTenantId(), e);
 //                }
 //            }
-            String intention = getIntention(callPhoneRes.getIntent());
+            String intention = null;
+            String intentionDegree = null;
+            if (StringUtils.isNotBlank(callPhoneRes.getDialogue())) {
+                log.info("【验证】意向度来源=自家AI, uuid={}, dialogueLength={}", callPhoneRes.getUuid(),
+                        StringUtils.isBlank(callPhoneRes.getDialogue()) ? 0 : callPhoneRes.getDialogue().length());
+                try {
+                    intentionDegree = crmCustomerAnalyzeService.aiIntentionDegree(
+                            callPhoneRes.getDialogue(),
+                            now().getLong(ChronoField.MILLI_OF_SECOND)
+                    );
+                    log.info("【验证】意向度结果={}, uuid={}", intentionDegree, callPhoneRes.getUuid());
+                    intention = getIntention(intentionDegree);
+                } catch (Exception e) {
+                    log.error("easyCall意向度AI解析失败,uuid={},将使用意向未知兜底", callPhoneRes.getUuid(), e);
+                }
+            }
+            // 2) 最终兜底:意向未知
+//            String intention = getIntention(callPhoneRes.getIntent());
             if (StringUtils.isEmpty(intention)) {
                 intention = "0";
             }
@@ -1452,8 +1560,10 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         companyWxClient.setDialogId(companyVoiceRoboticWx.getWxDialogId());
         if (Integer.valueOf(2).equals(robotic.getIsWeCom())) {
             companyWxClient.setCompanyUserId(qwUser.getCompanyUserId());
+            companyWxClient.setCompanyId(qwUser.getCompanyId());
         } else if (Integer.valueOf(1).equals(robotic.getIsWeCom())) {
             companyWxClient.setCompanyUserId(companyWxAccount.getCompanyUserId());
+            companyWxClient.setCompanyId(companyWxAccount.getCompanyId());
         }
         CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(companyWxClient.getCustomerId());
         companyWxClient.setNickName(crmCustomer.getCustomerName());
@@ -1515,8 +1625,10 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             companyWxClient.setDialogId(wx.getWxDialogId());
             if (robotic.getIsWeCom() == 2) {
                 companyWxClient.setCompanyUserId(qwMap.get(wx.getAccountId()).getCompanyUserId());
+                companyWxClient.setCompanyId(qwMap.get(wx.getAccountId()).getCompanyId());
             } else {
                 companyWxClient.setCompanyUserId(accountMap.get(wx.getAccountId()).getCompanyUserId());
+                companyWxClient.setCompanyId(accountMap.get(wx.getAccountId()).getCompanyId());
             }
             companyWxClient.setNickName(crmCustomer.getCustomerName());
             companyWxClient.setPhone(crmCustomer.getMobile());
@@ -1588,6 +1700,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             client.setRoboticWxId(companyVoiceRoboticWx.getId());
             client.setCompanyId(companyVoiceRoboticWx.getAccount().getCompanyId());
             client.setCompanyUserId(companyVoiceRoboticWx.getAccount().getCompanyUserId());
+            client.setCompanyId(companyVoiceRoboticWx.getAccount().getCompanyId());
             CompanyWxAccount account = new CompanyWxAccount();
             account.setId(companyVoiceRoboticWx.getAccount().getId());
             account.setAllocateNum(companyVoiceRoboticWx.getAccount().getAllocateNum() + 1);
@@ -1784,8 +1897,12 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return new ArrayList<>();
         }
         List<CompanyAiWorkflowExecLog> callLogs = logs.stream().filter(a -> "外呼".equals(a.getNodeName())).collect(Collectors.toList());
-        HashMap<Long,String> callContentMap = selectCallContentByCallLogs(callLogs);
-
+        HashMap<Long,String> callContentMap;
+        if (null != callLogs && !callLogs.isEmpty()) {
+            callContentMap = selectCallContentByCallLogs(callLogs);
+        } else {
+            callContentMap = new HashMap<>();
+        }
         return logs.stream().map(log -> {
             WorkflowExecRecordVo.NodeExecLogVo vo = new WorkflowExecRecordVo.NodeExecLogVo();
             vo.setId(log.getId());

+ 85 - 9
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -1,23 +1,23 @@
 package com.fs.company.service.impl;
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
-import com.fs.company.domain.CompanyAiWorkflowExec;
-import com.fs.company.domain.CompanyAiWorkflowExecLog;
-import com.fs.company.domain.CompanyWorkflow;
-import com.fs.company.domain.CompanyWorkflowNode;
-import com.fs.company.mapper.CompanyAiWorkflowExecLogMapper;
-import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
-import com.fs.company.mapper.CompanyWorkflowMapper;
-import com.fs.company.mapper.CompanyWorkflowNodeMapper;
+import com.fs.company.domain.*;
+import com.fs.company.mapper.*;
 import com.fs.company.param.ExecutionContext;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.service.IWorkflowNode;
+import com.fs.company.service.easycall.IEasyCallService;
 import com.fs.company.service.impl.call.node.WorkflowNodeFactory;
+import com.fs.company.vo.AiCallConfigVO;
 import com.fs.company.vo.ExecutionResult;
+import com.fs.company.vo.easycall.EasyCallCreateTaskParam;
+import com.fs.company.vo.easycall.EasyCallTaskVO;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
 import com.fs.wxcid.utils.TenantHelper;
@@ -60,6 +60,24 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
     @Autowired
     private CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
 
+    @Autowired
+    CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
+
+    @Autowired
+    IEasyCallService easyCallService;
+
+    @Autowired
+    CompanySiptaskInfoMapper companySiptaskInfoMapper;
+
+    @Autowired
+    CompanyWxAccountMapper companyWxAccountMapper;
+
+    @Autowired
+    CompanyWxClientMapper companyWxClientMapper;
+
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
     /**
      * 初始化工作流
      * 创建工作流实例并保存初始状态
@@ -84,7 +102,8 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
                     definition.getStartNodeKey(), context, definition);
 
             log.info("工作流初始化成功: {} -> {}", workflowInstanceId, workflowDefinitionId);
-
+            //为任务创建sip任务并存入表数据
+            createSipTask(Long.parseLong(inputVariables.get("roboticId").toString()),workflowDefinitionId);
             return ExecutionResult.success()
                     .nextNodeKey(definition.getStartNodeKey())
                     .workflowInstanceId(workflowInstanceId).build();
@@ -547,4 +566,61 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
 
     }
 
+    /**
+     * 创建SIP任务
+     * @param roboticId
+     * @param workFlowId
+     */
+    public Long createSipTask(Long roboticId, Long workFlowId) {
+        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;
+    }
+
 }

+ 64 - 43
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java

@@ -1,5 +1,6 @@
 package com.fs.company.service.impl;
 
+import cn.hutool.http.HttpUtil;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -13,6 +14,7 @@ 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.CidIpadServer;
 import com.fs.wxcid.domain.CidIpadServerUser;
 import com.fs.wxcid.domain.WxContact;
 import com.fs.wxcid.dto.friend.ContactItem;
@@ -34,8 +36,10 @@ import com.fs.wxcid.service.impl.LoginServiceImpl;
 import com.fs.wxcid.service.impl.UserServiceImpl;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import com.fs.company.service.impl.call.node.AiAddWxTaskNewNode;
 
 import java.time.LocalDateTime;
 import java.util.*;
@@ -101,7 +105,11 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     @Autowired
     CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
 
+    @Value("${wx.websocket.url:ws://localhost:7113/app/webSocket}")
+    private String webSocketUrl;
 
+    @Value("${wx.api.url:http://localhost:7113}")
+    private String wxApiUrl;
     /**
      * 查询企微账号
      *
@@ -141,6 +149,11 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     @Override
     public int insertCompanyWxAccount(CompanyWxAccount companyWxAccount)
     {
+        //校验添加微信前缀唯一性
+        CompanyWxAccount wxAccount = companyWxAccountMapper.selectCompanyWxAccountByWxRemark(companyWxAccount.getWxRemark());
+        if(null != wxAccount){
+            throw new RuntimeException("微信前缀已存在,请更换后重试");
+        }
         companyWxAccount.setCreateTime(DateUtils.getNowDate());
         return companyWxAccountMapper.insertCompanyWxAccount(companyWxAccount);
     }
@@ -259,13 +272,17 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
         if ( cidServerId==null ){
             return R.error("请先绑定cid服务");
         }
-        CompanyAiWorkflowServer cidServer = companyAiWorkflowServerMapper.selectCompanyAiWorkflowServerById(companyUser.getCidServerId());
-
-        Long serverId = cidIpadServerService.selectQwIpadServerByAddressId(addressId,cidServer.getGroupNo());
-        if (serverId==null){
-            return  R.error(501,"该地区服务器剩余数量不足");
+        CidIpadServer count = cidIpadServerService.getOne(new QueryWrapper<CidIpadServer>().gt("count", 0).last(" limit 1"));
+        if (count==null){
+            return  R.error(501,"回调服务器不足,请联系管理员");
         }
-        account.setServerId(serverId);
+//        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);
 
@@ -274,7 +291,7 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
         qwIpadServerUser.setCompanyUserId(companyUser.getUserId());
         qwIpadServerUser.setCompanyId(companyUser.getCompanyId());
         qwIpadServerUser.setCreateTime(new Date());
-        qwIpadServerUser.setServerId(serverId);
+        qwIpadServerUser.setServerId(count.getId());
         qwIpadServerUser.setQwUserId(account.getId());
         cidIpadServerUserService.save(qwIpadServerUser);
         return null;
@@ -332,36 +349,26 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
         account.setSyncFriendTime(LocalDateTime.now());
 
         try {
-            // 3. 清理Redis缓存
-            String key = FRIEND_KEY + accountId;
-            friendListRedis.deleteObject(key);
-
-            // 4. 获取好友列表并过滤
-            GetContactListParam param = new GetContactListParam();
-            param.setAccountId(accountId);
-            ContactListResponse response = friendService.getContactListNotKey(param);
-            List<String> friendList = getFilteredFriendList(response);
-            if (CollectionUtils.isEmpty(friendList)) {
-                log.info("账号暂无需要同步的微信好友/群聊,accountId={}", accountId);
-                updateById(account);
-                return;
+            // 3. 调用WebSocket发送同步指令
+            String wxNo = account.getWxNo();
+            if (wxNo != null && !wxNo.isEmpty()) {
+                sendSyncContactCommand(wxNo);
+            } else {
+                log.warn("微信账号wxNo为空,无法发送同步指令,accountId={}", accountId);
             }
 
-            // 5. 缓存好友列表到Redis
-            friendListRedis.setCacheObject(key, friendList);
-
-            // 6. 同步联系人信息
-            syncContactDetails(accountId, account, friendList);
-
-            // 7. 更新账号信息
+            // 4. 更新账号信息
             updateById(account);
 
-            log.info("微信信息同步完成,accountId={},同步联系人数量={}", accountId, friendList.size());
+//            log.info("微信信息同步完成,accountId={},同步联系人数量={}", accountId, friendList.size());
         } catch (Exception e) {
             log.error("同步微信信息异常,accountId={}", accountId, e);
             throw e;
         }
     }
+    private void sendSyncContactCommand(String wxNo) {
+        HttpUtil.get(wxApiUrl + "/app/common/syncWx?wxId=" + wxNo);
+    }
 
     /**
      * 获取过滤后的好友列表
@@ -576,17 +583,32 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
      * 加微成功后触发工作流继续执行
      * @param wxClientId 加微客户ID
      */
-    private void triggerWorkflowOnAddWxSuccess(Long wxClientId) {
+    public void triggerWorkflowOnAddWxSuccess(Long wxClientId) {
         try {
-            // 查找等待中的加微工作流实例
-            CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+            // 先查老类型的等待中工作流实例
+//            CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+//                    wxClientId,
+//                    ExecutionStatusEnum.WAITING.getValue(),
+//                    NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+//            boolean isNewNodeType = false;
+//            // 老类型未找到,再查新类型
+//            if (waitingExec == null) {
+            CompanyAiWorkflowExec  waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
                     wxClientId,
                     ExecutionStatusEnum.WAITING.getValue(),
-                    NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+                    NodeTypeEnum.AI_ADD_WX_TASK_NEW.getValue());
+//                isNewNodeType = true;
+//            }
+
+            if (waitingExec == null) {
+                log.info("未找到等待中的加微工作流实例 - wxClientId: {}", wxClientId);
+                return;
+            }
+
             //查询工作流加微执行日志是否未更新状态
             CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
             queryP.setWorkflowInstanceId(waitingExec.getWorkflowInstanceId());
-            queryP.setNodeType(NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+            queryP.setNodeType(NodeTypeEnum.AI_ADD_WX_TASK_NEW.getValue());
             queryP.setStatus(ExecutionStatusEnum.WAITING.getValue());
             List<CompanyAiWorkflowExecLog> companyAiWorkflowExecLogs = companyAiWorkflowExecLogMapper.selectCompanyAiWorkflowExecLogList(queryP);
             companyAiWorkflowExecLogs.forEach(log -> {
@@ -594,10 +616,6 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
                         companyAiWorkflowExecLogMapper.updateById(log);
                     }
             );
-            if (waitingExec == null) {
-                log.info("未找到等待中的加微工作流实例 - wxClientId: {}", wxClientId);
-                return;
-            }
 
             String workflowInstanceId = waitingExec.getWorkflowInstanceId();
             String currentNodeKey = waitingExec.getCurrentNodeKey();
@@ -605,21 +623,24 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
             log.info("加微成功回调,尝试触发工作流继续执行 - workflowInstanceId: {}, nodeKey: {}, wxClientId: {}",
                     workflowInstanceId, currentNodeKey, wxClientId);
 
-            // 互斥检查:如果已经被执行过(超时路径或其他回调),则不再执行
-            if (!AiAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId)) {
+            // 互斥检查:根据节点类型使用对应的互斥方法
+            boolean canExecute;
+//            if (isNewNodeType) {
+            canExecute = AiAddWxTaskNewNode.tryMarkAsExecuted(workflowInstanceId, wxClientId);
+//            } else {
+//                canExecute = AiAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId);
+//            }
+            if (!canExecute) {
                 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"); // 回调触发
+            inputData.put("triggerType", "callback");
 
             companyWorkflowEngine.resumeFromBlockingNode(workflowInstanceId, currentNodeKey, inputData);
 

+ 120 - 0
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java

@@ -9,18 +9,22 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.CompanyConfig;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.mapper.CompanyVoiceRoboticMapper;
+import com.fs.company.mapper.EasyCallMapper;
 import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.param.InboundCallbackParam;
 import com.fs.company.service.ICompanyConfigService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.IGeneralCustomerEntryService;
 import com.fs.company.util.PhoneNumberUtil;
 import com.fs.company.vo.DictVO;
+import com.fs.company.vo.InboundCallInfo;
 import com.fs.config.ai.AiHostProper;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.domain.CrmCustomerProperty;
@@ -80,6 +84,19 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
     CompanyVoiceRoboticServiceImpl companyVoiceRoboticServiceImpl;
     @Autowired
     CrmCustomerPropertyServiceImpl crmCustomerPropertyService;
+
+    @Autowired
+    EasyCallMapper easyCallMapper;
+
+    @Autowired
+    private RedisCache redisCache2;
+
+    /** 呼入回调 chatContent(对话内容)重试队列 Redis key 前缀,value 为已重试次数 */
+    private static final String INBOUND_CHAT_CONTENT_RETRY_KEY = "inbound:chat:retry:";
+    /** chatContent 对话内容等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
+    private static final int INBOUND_CHAT_CONTENT_MAX_RETRY = 5;
+    /** chatContent 每次重试等待时长(毫秒) */
+    private static final long INBOUND_CHAT_CONTENT_RETRY_INTERVAL_MS = 30000L;
     /**
      * 录入客户
      *
@@ -406,5 +423,108 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
         }
         return !now.isBefore(start) || !now.isAfter(end);
     }
+    @Override
+    @Async("cidWorkFlowExecutor")
+    public void inboundCallback(InboundCallbackParam param){
+        try {
+            Thread.sleep(5000L);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException(e);
+        }
+        if (param == null || StringUtils.isBlank(param.getUuid())){
+            return;
+        }
+        InboundCallInfo info  = easyCallMapper.selectInboundCallbackInfoByUuid(param.getUuid());
+        // chatContent(对话内容)由对方异步写入,回调时可能尚未赋值,进入延迟重试队列等待
+        if (info == null || isChatContentEmpty(info.getChatContent())) {
+            String retryKey = INBOUND_CHAT_CONTENT_RETRY_KEY + param.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = 0;
+            }
+            if (retryCount < INBOUND_CHAT_CONTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("呼入回调chatContent对话内容暂未写入,uuid={},第{}次放入延迟重试队列", param.getUuid(), retryCount + 1);
+                doRetryInboundCallback(param, retryCount + 1);
+            } else {
+                // 超过最大重试次数,以 chatContent 为空兜底继续处理
+                log.warn("呼入回调chatContent对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", INBOUND_CHAT_CONTENT_MAX_RETRY, param.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleInboundCallback(param, info);
+            }
+            return;
+        }
+        // chatContent 已有值,直接正常处理
+        redisCache2.deleteObject(INBOUND_CHAT_CONTENT_RETRY_KEY + param.getUuid());
+        doHandleInboundCallback(param, info);
+    }
+
+    /**
+     * 判断 chatContent 对话内容是否为空(null、空字符串、空数组 "[]" 均视为无对话内容)
+     */
+    private boolean isChatContentEmpty(String chatContent) {
+        if (StringUtils.isBlank(chatContent)) {
+            return true;
+        }
+        String trimmed = chatContent.trim();
+        return "[]".equals(trimmed);
+    }
+
+    /**
+     * 延迟重试处理呼入回调(等待 chatContent 对话内容异步写入完成)
+     * 每次重试前等待 {@link #INBOUND_CHAT_CONTENT_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
+     */
+    @Async("cidWorkFlowExecutor")
+    public void doRetryInboundCallback(InboundCallbackParam param, int currentRetry) {
+        try {
+            Thread.sleep(INBOUND_CHAT_CONTENT_RETRY_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("呼入回调chatContent重试等待被中断, uuid={}", param.getUuid());
+            return;
+        }
+        log.info("呼入回调chatContent重试第{}次开始, uuid={}", currentRetry, param.getUuid());
+        InboundCallInfo info = easyCallMapper.selectInboundCallbackInfoByUuid(param.getUuid());
+        if (info == null || isChatContentEmpty(info.getChatContent())) {
+            // chatContent 仍为空,继续判断是否还有剩余重试次数
+            String retryKey = INBOUND_CHAT_CONTENT_RETRY_KEY + param.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = currentRetry;
+            }
+            if (retryCount < INBOUND_CHAT_CONTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("呼入回调chatContent对话内容仍未写入,uuid={},第{}次继续延迟重试", param.getUuid(), retryCount + 1);
+                doRetryInboundCallback(param, retryCount + 1);
+            } else {
+                log.error("呼入回调chatContent对话内容在{}次重试后仍为空,uuid={},以对话为空兜底处理", INBOUND_CHAT_CONTENT_MAX_RETRY, param.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleInboundCallback(param, info);
+            }
+            return;
+        }
+        // chatContent 已写入完成,正常处理
+        log.info("呼入回调chatContent重试第{}次成功获取到对话内容,uuid={}", currentRetry, param.getUuid());
+        redisCache2.deleteObject(INBOUND_CHAT_CONTENT_RETRY_KEY + param.getUuid());
+        doHandleInboundCallback(param, info);
+    }
 
+    /**
+     * 执行呼入回调核心业务处理(组装客户入参并录入)
+     * 供 {@link #inboundCallback} 和重试逻辑统一调用
+     */
+    private void doHandleInboundCallback(InboundCallbackParam param, InboundCallInfo info) {
+        if (info == null) {
+            log.error("呼入回调信息未查询到结果, uuid={}", param.getUuid());
+            return;
+        }
+        EntryCustomerParam entry = new EntryCustomerParam();
+        entry.setTraceId(param.getUuid());
+        entry.setCompanyId(info.getFsCompanyId());
+        entry.setSceneType(info.getFsSceneType());
+        entry.setMobile(info.getCaller());
+        entry.setDialogue(info.getChatContent());
+        entryCustomer(entry);
+    }
 }

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

@@ -13,7 +13,7 @@ import com.fs.company.service.IWorkflowNode;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
-import com.fs.wxcid.utils.TenantHelper;
+import com.fs.enums.TaskTypeEnum;
 import lombok.extern.slf4j.Slf4j;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
@@ -61,9 +61,6 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
 
     @Override
     public ExecutionResult execute(ExecutionContext context) {
-
-        log.info("execute得到租户id:{}", TenantHelper.getTenantId());
-
         if(!runnable(context) && getType() != NodeTypeEnum.END){
             log.info("当前流程已到达结束节点,节点执行失败:- {},- {} -,{}" , nodeName, nodeKey, context.getWorkflowInstanceId());
             return null;
@@ -117,7 +114,7 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
         }
         // 校验节点执行是否符合时间设置
         CompanyAiWorkflowExec timeAvailable = companyAiWorkflowExecMapper.selectExecWithTimeAvailableByInstanceId(context.getWorkflowInstanceId());
-        
+
         if (timeAvailable == null) {
             CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());
             if (exec == null) {
@@ -134,7 +131,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();
@@ -172,6 +169,8 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
                     updateLogStatusIfExist(context,ExecutionStatusEnum.WAITING_DO_CALL,ExecutionStatusEnum.SUCCESS);
                 } else if(getType().equals(NodeTypeEnum.AI_ADD_WX_TASK)){
                     updateLogStatusIfExist(context,ExecutionStatusEnum.WAITING,ExecutionStatusEnum.SUCCESS);
+                } else if(getType().equals(NodeTypeEnum.AI_ADD_WX_TASK_NEW)){
+                    updateLogStatusIfExist(context,ExecutionStatusEnum.WAITING,ExecutionStatusEnum.SUCCESS);
                 } else if (getType().equals(NodeTypeEnum.OUTBOUND_TASK)) {
                     updateLogStatusIfExist(context,ExecutionStatusEnum.WAITING_DO_CALL,ExecutionStatusEnum.SUCCESS);
                 }
@@ -206,7 +205,8 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             fExecLog.setStatus(status.getValue());
             fExecLog.setEndTime(new Date());
             long durationInMillis = fExecLog.getEndTime().getTime() - fExecLog.getStartTime().getTime();
-            fExecLog.setDuration(durationInMillis);
+            // 兜底防护:确保duration不为负数;当计算结果<=0时设为1毫秒以显得更加真实
+            fExecLog.setDuration(durationInMillis > 0 ? durationInMillis : 1);
             companyAiWorkflowExecLogMapper.updateById(fExecLog);
         }else{
             log.error("未更新到节点状态:context:{},findS:{},targetS:{}",context,findStatus,status);
@@ -249,7 +249,7 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
         context.setVariable("node_end_time_" + nodeKey, endTime);
         log.info("Completed execution of node: {} ({})", nodeKey, nodeName);
         if (result == null) {
-            log.error("节点 {} 执行结果为 null,跳过记录执行日志", nodeKey);
+            log.warn("节点 {} 执行结果为 null,跳过记录执行日志", nodeKey);
             return;
         }
         int logStatus = result.getStatus().getValue();
@@ -365,14 +365,21 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             logEntry.setErrorMessage(result.getErrorMessage());
             logEntry.setCreatedTime(new Date());
             Long startTime = context.getVariable("node_start_time_" + nodeKey, Long.class);
+            Long endTime = context.getVariable("node_end_time_" + nodeKey, Long.class);
+            // 当startTime为null但endTime不为null时(如获取锁失败、preExecute未执行),
+            // 不能用new Date()作为startTime的fallback,因为此时new Date()可能晚于endTime,
+            // 导致duration计算为负数。应将startTime对齐到endTime,表示节点未能正常启动
             if (null != startTime) {
                 logEntry.setStartTime(new Date(startTime));
+            } else if (null != endTime) {
+                logEntry.setStartTime(new Date(endTime));
             } else {
                 logEntry.setStartTime(new Date());
             }
-            Long endTime = context.getVariable("node_end_time_" + nodeKey, Long.class);
             if (null != endTime) {
                 logEntry.setEndTime(new Date(endTime));
+            } else if (null != startTime) {
+                logEntry.setEndTime(new Date(startTime));
             } else {
                 logEntry.setEndTime(new Date());
             }
@@ -380,6 +387,11 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             if (null != startTime && null != endTime) {
                 duration = endTime - startTime;
             }
+            // 兜底防护:确保duration不为负数;当使用fallback值(startTime或endTime为null)时,
+            // 最小耗时设为1毫秒以显得更加真实
+            if (duration <= 0) {
+                duration = (null != startTime && null != endTime) ? 0 : 1;
+            }
             logEntry.setDuration(duration);
             return logEntry;
         } catch (JsonProcessingException e) {
@@ -430,6 +442,26 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
         companyAiWorkflowExecMapper.updateByWorkflowInstanceId(update);
     }
 
+    /**
+     * 任务完成判定
+     * @param context
+     */
+    public void taskFinish(ExecutionContext context){
+        //判定是否任务完成了更新任务的状态为执行完成
+        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
+        if(null != roboticBusiness){
+            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
+            if(Integer.valueOf(0).equals(i)){
+                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
+                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
+                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+                    robotic.setId(roboticBusiness.getRoboticId());
+                    robotic.setTaskStatus(3);
+                    companyVoiceRoboticMapper.updateById(robotic);
+                }
+            }
+        }
+    }
     public CompanyAiWorkflowExec getWorkflowExec(String workflowInstanceId) {
         CompanyAiWorkflowExec companyAiWorkflowExec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(workflowInstanceId);
         if (null == companyAiWorkflowExec) {
@@ -487,7 +519,6 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             execPointNextNode(nextContext);
             node.execute(nextContext);
         };
-        // 线程池已配置 TaskDecorator 自动传递租户ID,无需手动包装
         if (executor != null) {
             CompletableFuture.runAsync(nextTask, executor);
         } else {

+ 326 - 0
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java

@@ -0,0 +1,326 @@
+package com.fs.company.service.impl.call.node;
+
+import cn.hutool.core.util.RandomUtil;
+import com.alibaba.fastjson.JSONObject;
+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.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;
+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.service.impl.CrmCustomerServiceImpl;
+import com.fs.enums.ExecutionStatusEnum;
+import com.fs.enums.NodeTypeEnum;
+import com.fs.system.service.ISysConfigService;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.mapper.CidIpadServerMapper;
+import com.fs.wxcid.mapper.WxContactMapper;
+import com.fs.wxwork.service.WxIpadService;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author MixLiu
+ * @date 2026/04/22 13:15
+ * @description AI添加微信任务节点(新)
+ */
+@Slf4j
+public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
+
+    private static final CompanyWxClientMapper companyWxClientMapper = SpringUtils.getBean(CompanyWxClientMapper.class);
+    @SuppressWarnings("unchecked")
+    private static final RedisCacheT<String> redisCache = SpringUtils.getBean(RedisCacheT.class);
+    private static final WxContactMapper wxContactMapper = SpringUtils.getBean(WxContactMapper.class);
+    private static final CompanyWxAccountMapper companyWxAccountMapper = SpringUtils.getBean(CompanyWxAccountMapper.class);
+    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 ObjectPlaceholderResolver objectPlaceholderResolver = SpringUtils.getBean(ObjectPlaceholderResolver.class);
+    private static final ISysConfigService sysConfigService = SpringUtils.getBean(ISysConfigService.class);
+    private static final CidIpadServerMapper cidIpadServerMapper = SpringUtils.getBean(CidIpadServerMapper.class);
+    private static final WxIpadService wxService = SpringUtils.getBean(WxIpadService.class);
+//    private static final String ADDWX_POST_URL = "/app/common/addWxAction";
+    /**
+     * 默认加微超时时间(分钟)
+     */
+    private static final int DEFAULT_ADD_WX_TIMEOUT_MINUTES = 30;
+
+    public AiAddWxTaskNewNode(String nodeKey, String nodeName, Map<String, Object> properties) {
+        super(nodeKey, nodeName, properties);
+    }
+
+    @Override
+    public NodeTypeEnum getType() {
+        return NodeTypeEnum.AI_ADD_WX_TASK_NEW;
+    }
+
+    @Override
+    public Boolean isAsync() {
+        return true;
+    }
+
+    /**
+     * 执行加微节点逻辑(准备数据并发起加微请求)
+     *
+     * @param context 执行上下文
+     * @return 执行结果
+     */
+    @Override
+    protected ExecutionResult doExecute(ExecutionContext context) {
+        if (!isAsync()) {
+            return ExecutionResult.failure().nextNodeKey(null).build();
+        }
+        try {
+            // 设置加微话术
+            CompanyWorkflowNode node = context.getVariable("currentNode", CompanyWorkflowNode.class) == null
+                    ? getNodeByKey(nodeKey)
+                    : context.getVariable("currentNode", CompanyWorkflowNode.class);
+            String nodeConfig = node.getNodeConfig();
+            AiAddWxConfigVO addWxConfig = nodeConfig == null ? null : JSONObject.parseObject(nodeConfig, AiAddWxConfigVO.class);
+
+            if (addWxConfig == null || addWxConfig.getDialogId() == null) {
+                throw new CustomException("加微节点未配置加微话术,执行失败");
+            }
+
+            WxContact wxQuery = companyAiWorkflowExecMapper.selectWxContectByWorkflowInstanceId(context.getWorkflowInstanceId());
+            wxQuery.setRemark(wxQuery.getRemark() + RandomUtil.randomNumbers(10));
+            wxQuery.setNickName(wxQuery.getRemark());
+            wxQuery.setFriends(0);
+            wxContactMapper.insert(wxQuery);
+
+            CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
+            CompanyWxClient update = new CompanyWxClient();
+            update.setDialogId(addWxConfig.getDialogId());
+            update.setId(roboticBusiness.getWxClientId());
+            companyWxClientMapper.updateCompanyWxClient(update);
+            super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.PAUSED);
+            pendingAddWx(wxQuery.getAccountId(), wxQuery.getRemark(),
+                    wxQuery.getPhone(),
+                    addWxConfig.getDialogId(),
+                    wxQuery.getCrmUserId(),
+                    context.getWorkflowInstanceId(),
+                    context.getCurrentNodeKey());
+            return ExecutionResult.paused()
+                    .outputData(context.getVariables())
+                    .nextNodeKey("").build();
+
+        } catch (Exception e) {
+            log.error("准备加微任务数据异常 流程:{}:节点:{}执行失败,", context.getWorkflowInstanceId(), nodeKey, e);
+            super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.INTERRUPT);
+            return ExecutionResult.failure().errorMessage("准备加微任务数据异常: " + e.getMessage()).build();
+        }
+    }
+
+    /**
+     * 收到加微回调后,继续判定和执行下一步动作
+     *
+     * @param context 执行上下文
+     * @return 执行结果
+     */
+    @Override
+    protected ExecutionResult doContinue(ExecutionContext context) {
+        CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());
+        if (exec == null) {
+            log.warn("doContinue: 工作流执行实例不存在 - workflowInstanceId: {}", context.getWorkflowInstanceId());
+            return null;
+        }
+        List<CompanyWorkflowEdge> edges = companyWorkflowEdgeMapper.selectListByWorkflowIdAndNodeKey(exec.getWorkflowId(), nodeKey);
+        if (edges == null || edges.isEmpty()) {
+            log.warn("doContinue: 未找到出边 - workflowInstanceId: {}, nodeKey: {}", context.getWorkflowInstanceId(), nodeKey);
+            return null;
+        }
+
+        // 获取业务数据
+        CompanyVoiceRoboticBusiness business = super.getRoboticBusiness(context.getWorkflowInstanceId());
+        // 获取加微记录
+        CompanyWxClient wxClient = companyWxClientMapper.selectById(business.getWxClientId());
+
+        log.info("收到加微回调 - workflowInstanceId: {}, wxClientId: {}, isAdd: {}",
+                context.getWorkflowInstanceId(), business.getWxClientId(), wxClient != null ? wxClient.getIsAdd() : null);
+
+        // 判断加微是否成功 (isAdd: 0否 1是 2待添加 3作废)
+        boolean addSuccess = wxClient != null && Integer.valueOf(1).equals(wxClient.getIsAdd());
+        // 回调加微成功,走成功分支出边
+        if (addSuccess) {
+            List<CompanyWorkflowEdge> cList = edges.stream().filter(a -> {
+                if (StringUtils.isBlank(a.getConditionExpr())) {
+                    return false;
+                }
+                List<AiCallWorkflowConditionVo> list = JSONObject.parseArray(a.getConditionExpr(), AiCallWorkflowConditionVo.class);
+                return list != null && !list.isEmpty() && list.get(0).isAdd();
+            }).collect(Collectors.toList());
+            if (null != cList && !cList.isEmpty() && nodeKey.equals(exec.getCurrentNodeKey())) {
+                super.runNextNode(context, cList.get(0));
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 完成加微动作,设置为 WAITING 并处理出边条件(延时/直通)
+     *
+     * @param workflowInstanceId 工作流实例ID
+     */
+    public void doneAddwx(String workflowInstanceId) {
+        ExecutionContext context = createExecutionContext(workflowInstanceId, nodeKey);
+        context.setVariable("lastNodeKey", nodeKey);
+        CompanyAiWorkflowExec exec = companyAiWorkflowExecMapper.selectByWorkflowInstanceId(context.getWorkflowInstanceId());
+        if (exec == null) {
+            log.error("doneAddwx: 工作流执行实例不存在 - workflowInstanceId: {}", workflowInstanceId);
+            return;
+        }
+        if (!exec.getCurrentNodeKey().equals(nodeKey)) {
+            log.error("当前节点已流转 ,目标:{},实际:{}", nodeKey, exec.getCurrentNodeKey());
+            return;
+        }
+        // 更新加微日志执行状态
+        super.updateLogStatusIfExist(context, ExecutionStatusEnum.PAUSED, ExecutionStatusEnum.WAITING);
+        super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), nodeKey, context, ExecutionStatusEnum.WAITING);
+        List<CompanyWorkflowEdge> edges = companyWorkflowEdgeMapper.selectListByWorkflowIdAndNodeKey(exec.getWorkflowId(), nodeKey);
+        if (edges == null) {
+            return;
+        }
+        edges.forEach(edge -> {
+            String conditionExpr = edge.getConditionExpr();
+            List<AiCallWorkflowConditionVo> conditions = StringUtils.isBlank(conditionExpr) ? null : JSONObject.parseArray(conditionExpr, AiCallWorkflowConditionVo.class);
+            if (null == conditions || conditions.isEmpty()) {
+                super.runNextNode(context, edge);
+            } else {
+                AiCallWorkflowConditionVo condition = conditions.get(0);
+                // 节点包含延时条件
+                if (null != condition.getAddTime() && !condition.isAdd()) {
+                    long l = System.currentTimeMillis() + condition.getAddTime() * 60 * 1000;
+                    String redisKey = getDelayAddWxKeyPrefix(exec.getCidGroupNo(), l) + workflowInstanceId;
+                    ExecutionContext nextContext = context.clone();
+                    nextContext.setCurrentNodeKey(edge.getTargetNodeKey());
+                    super.redisCache.setCacheObject(redisKey, nextContext);
+                }
+            }
+        });
+    }
+
+    /**
+     * 检查并标记已执行(互斥控制)
+     * 返回 true 表示当前是第一个执行的,可以继续;
+     * 返回 false 表示已被其他路径执行过,不再执行。
+     *
+     * @param workflowInstanceId 工作流实例ID
+     * @param wxClientId         加微客户ID
+     * @return 是否可以执行
+     */
+    public static boolean tryMarkAsExecuted(String workflowInstanceId, Long wxClientId) {
+        String executedKey = Constants.WORKFLOW_ADD_WX_EXECUTED + "new:" + workflowInstanceId + ":" + wxClientId;
+        String existingValue = redisCache.getCacheObject(executedKey);
+        if (existingValue != null) {
+            return false;
+        }
+        redisCache.setCacheObject(executedKey, "1");
+        redisCache.expire(executedKey, 3600);
+        return true;
+    }
+
+    /**
+     * 生成延时加微 Redis Key 前缀
+     *
+     * @param cidGroupNo CID分组号
+     * @param time       目标时间戳(毫秒),为 null 时取当前时间
+     * @return Redis Key 前缀
+     */
+    public static String getDelayAddWxKeyPrefix(Integer cidGroupNo, Long time) {
+        Date nowDay = new Date();
+        if (null != time) {
+            nowDay = new Date(time);
+        }
+        return String.format(DELAY_ADD_WX_NEW_KEY, cidGroupNo, nowDay.getHours(), nowDay.getMinutes());
+    }
+
+    /**
+     * 从节点配置获取超时时间(分钟)
+     */
+    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;
+    }
+
+    /**
+     * 发起加微请求
+     * @param accountId
+     * @param remark
+     * @param phone
+     * @param dialogId
+     * @param crmUserId
+     * @param instanceId
+     * @param nodeKey
+     */
+    private void pendingAddWx(Long accountId, String remark, String phone, Long dialogId, Long crmUserId,String instanceId,String nodeKey) {
+        try {
+            // 1. 获取基础数据
+            CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(accountId);
+            if (companyWxAccount == null) {
+                throw new CustomException("未找到对应的微信账号配置, accountId: " + accountId);
+            }
+
+            CompanyWxDialog dialog = companyWxDialogMapper.selectCompanyWxDialogById(dialogId);
+            if (dialog == null) {
+                throw new CustomException("未找到对应的对话模板, dialogId: " + dialogId);
+            }
+
+            CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(crmUserId);
+
+            // 2. 解析话术模板
+            String newTxt = objectPlaceholderResolver.resolvePlaceholders(crmCustomer, dialog.getTemplateDetails());
+
+            // 3. 构建请求参数
+            AddWxActionParam param = new AddWxActionParam();
+            param.setWxId(companyWxAccount.getWxNo());
+            param.setRemark(remark);
+            param.setPhone(phone);
+            param.setApplyMsg(newTxt);
+            param.setServerId(companyWxAccount.getServerId());
+            JSONObject bizJson = new JSONObject()
+                    .fluentPut("instanceId",instanceId)
+                    .fluentPut("nodeKey",nodeKey)
+                    .fluentPut("accountId",companyWxAccount.getId());
+            param.setBizJson(bizJson.toJSONString());
+            wxService.addWx(param);
+        } catch (Exception ex) {
+            throw new CustomException("发起加微请求异常, phone: " + phone + ", error: " + ex.getMessage());
+        }
+    }
+
+    @Override
+    protected void postExecute(ExecutionContext context, ExecutionResult result) {
+        super.postExecute(context, result);
+        // 仅当节点成功进入PAUSED状态时才执行doneAddwx,
+        // 避免在result为null(执行异常)或FAILURE(加微准备失败)时仍继续流转流程
+        if (result != null && result.getStatus() == ExecutionStatusEnum.PAUSED) {
+            doneAddwx(context.getWorkflowInstanceId());
+        }
+    }
+}

+ 81 - 40
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -6,11 +6,10 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.enums.BusinessTypeEnum;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
-import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
-import com.fs.company.mapper.CompanyWorkflowNodeMapper;
+import com.fs.company.mapper.*;
 import com.fs.company.param.CompanyVoiceRoboticCallBlacklistCheckParam;
 import com.fs.company.param.ExecutionContext;
+import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.service.ICompanyVoiceRoboticCallBlacklistService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.easycall.IEasyCallService;
@@ -27,9 +26,9 @@ 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.system.service.ISysConfigService;
-import com.fs.wxcid.utils.TenantHelper;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.*;
@@ -46,6 +45,9 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     private static final ICompanyVoiceRoboticService companyVoiceRoboticService = SpringUtils.getBean(ICompanyVoiceRoboticService.class);
     /** EasyCallCenter365 外呼服务 */
     private static final IEasyCallService easyCallService = SpringUtils.getBean(IEasyCallService.class);
+    private static final CompanyConfigMapper companyConfigMapper = SpringUtils.getBean(CompanyConfigMapper.class);
+    private static final CompanySiptaskInfoMapper companySiptaskInfoMapper = SpringUtils.getBean(CompanySiptaskInfoMapper.class);
+    private static final CompanyWorkflowEngine companyWorkflowEngine = SpringUtils.getBean(CompanyWorkflowEngine.class);
     /** 被叫人表 Mapper,用于获取手机号 */
     private static final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper = SpringUtils.getBean(CompanyVoiceRoboticCalleesMapper.class);
     private static final CompanyVoiceRoboticCallLogCallphoneServiceImpl companyVoiceRoboticCallLogCallphoneService = SpringUtils.getBean(CompanyVoiceRoboticCallLogCallphoneServiceImpl.class);
@@ -157,6 +159,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         //如果没有节点可执行通路 条件均不满足 则标记流程为中断
         if(runnableCount < 1){
             super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.INTERRUPT);
+            super.taskFinish(context);
         }
 
         return null;
@@ -203,7 +206,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())
@@ -261,6 +264,24 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     private void workflowCallPhoneOne4EasyCall(Long roboticId,Long calleeId, ExecutionContext context, AiCallConfigVO callConfigVo) {
 
         CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectById(roboticId);
+        String callBackUrl = "";
+        try{
+            CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(robotic.getCompanyId(), "cId.config");
+            CidPhoneConfig cidConf = JSONObject.parseObject(companyConfig.getConfigValue(), CidPhoneConfig.class);
+            if(null != cidConf && StringUtils.isNotBlank(cidConf.getCallbackUrl())){
+                callBackUrl = cidConf.getCallbackUrl();
+            }
+            //读取总后台配置
+            if(StringUtils.isBlank(callBackUrl)){
+                String s = configService.selectConfigByKey("cId.config");
+                JSONObject obj = JSONObject.parseObject(s);
+                if(null != obj && obj.containsKey("callbackUrl") && StringUtils.isNotBlank(obj.getString("callbackUrl"))){
+                    callBackUrl = obj.getString("callbackUrl");
+                }
+            }
+        } catch (Exception ex){
+            log.error("获取公司Cid配置失败:{}", ex);
+        }
         // 1. 生成回调唯一标识符,后续回调时通过此 uuid 匹配对应的流程实例
         String callBackUuid = UUID.randomUUID().toString();
         // 将回调信息写入 Redis,保存 1 天
@@ -294,43 +315,44 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         }
 
         // 3. 构建创建任务参数(AI 外呼模式:taskType=1)
-        EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
-        // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
-        createParam.setBatchName(robotic.getName() + "_" + context.getWorkflowInstanceId() + "_" + calleeId);
-        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());
-        // 技能组(转人工客服分组,可选)
+//        EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
+//        // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
+//        createParam.setBatchName(robotic.getName() + "_" + context.getWorkflowInstanceId() + "_" + calleeId);
+//        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());
 
-        JSONObject runParam = (JSONObject) JSON.toJSON(createParam);
-        runParam.put("companyId", robotic.getCompanyId());
-        CompanyVoiceRoboticCallLogCallphone addLog = CompanyVoiceRoboticCallLogCallphone.initCallLog(
-                runParam.toJSONString(), calleeId, roboticId, robotic.getCompanyId());
+
         // 4. 调用 EasyCallCenter365 创建任务接口
         // companyId 传 null 是因为 EasyCallCenter365 是全局地址,不需要按公司隔离
-        log.info("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务 - workflowInstanceId: {}, calleeId: {}",
-                context.getWorkflowInstanceId(), calleeId);
-        EasyCallTaskVO task = easyCallService.createTask(createParam, null);
-        if (task == null || task.getBatchId() == null) {
-            log.error("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务失败 - workflowInstanceId: {}",
-                    context.getWorkflowInstanceId());
-            throw new RuntimeException("EasyCallCenter365 创建任务失败");
+//        log.info("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务 - workflowInstanceId: {}, calleeId: {}",
+//                context.getWorkflowInstanceId(), calleeId);
+//        EasyCallTaskVO task = easyCallService.createTask(createParam, null);
+//        if (task == null || task.getBatchId() == null) {
+//            log.error("workflowCallPhoneOne4EasyCall: 创建 EasyCall 任务失败 - workflowInstanceId: {}",
+//                    context.getWorkflowInstanceId());
+//            throw new RuntimeException("EasyCallCenter365 创建任务失败");
+//        }
+        Long batchId = getTaskBatchId(robotic.getId(), context.getCurrentNodeKey(), context.getWorkflowInstanceId());
+        if(null == batchId ){
+            log.error("workflowCallPhoneOne4EasyCall: 获取 EasyCall 任务批次ID失败 - workflowInstanceId: {}",context.getWorkflowInstanceId());
+            throw new RuntimeException("任务批次ID失败");
         }
-        Long batchId = task.getBatchId();
+
         log.info("workflowCallPhoneOne4EasyCall: EasyCall 任务创建成功 - batchId: {}", batchId);
 
         // 5. 将被叫号码加入任务名单(使用通用追加接口,支持传入业务数据)
@@ -340,8 +362,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         EasyCallPhoneItemVO phoneItem = new EasyCallPhoneItemVO();
         phoneItem.setPhoneNum(callees.getPhone());
         // bizJson 默认传入客户姓名占位,运行时可根据实际业务填充
-        log.info("workflowCallPhoneOne4EasyCall得到租户id::{}",TenantHelper.getTenantId());
-        phoneItem.setBizJson(new JSONObject().fluentPut("custName", callees.getUserName()).fluentPut("callBackUuid",callBackUuid).fluentPut("tenantId", TenantHelper.getTenantId()));
+        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: {}",
@@ -350,6 +371,10 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         // 6. 启动外呼任务
         easyCallService.startTask(batchId, null);
         log.info("workflowCallPhoneOne4EasyCall: 任务启动成功 - batchId: {}", batchId);
+        JSONObject runParam = (JSONObject) JSON.toJSON(addListParam);
+        runParam.put("companyId", robotic.getCompanyId());
+        CompanyVoiceRoboticCallLogCallphone addLog = CompanyVoiceRoboticCallLogCallphone.initCallLog(
+                runParam.toJSONString(), calleeId, roboticId, robotic.getCompanyId());
         addLog.setStatus(1);
         addLog.setCallbackUuid(callBackUuid);
         companyVoiceRoboticCallLogCallphoneService.asyncInsertCompanyVoiceRoboticCallLog(addLog);
@@ -358,7 +383,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
     }
 
     private boolean checkPhoneCallLimit(Long businessId){
-        String json = configService.selectConfigByKey("cid.config");
+        String json = configService.selectConfigByKey("cId.config");
         if(StringUtils.isNotEmpty(json)){//数据存在
             //转换数据
             CidPhoneConfig config =JSONObject.parseObject(json,CidPhoneConfig.class);
@@ -377,6 +402,22 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         return false;
     }
 
+    /**
+     * 获取siptaskid
+     * @param roboticId
+     * @param nodeKey
+     * @param workflowInstanceId
+     * @return
+     */
+    public Long getTaskBatchId(Long roboticId, String nodeKey, String workflowInstanceId){
+        CompanySiptaskInfo 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());
+    }
 //    @Override
 //    protected void postExecute(ExecutionContext context, ExecutionResult result) {
 //        super.postExecute(context, result);

+ 18 - 16
fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java

@@ -1,6 +1,7 @@
 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;
@@ -43,21 +44,22 @@ public class EndNode extends AbstractWorkflowNode {
      */
     @Override
     protected void postExecute(ExecutionContext context, ExecutionResult result) {
-      super.postExecute(context, result);
-      super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.SUCCESS);
-      //判定是否任务完成了更新任务的状态为执行完成
-        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
-        if(null != roboticBusiness){
-            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
-            if(Integer.valueOf(0).equals(i)){
-                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
-                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
-                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
-                    robotic.setId(roboticBusiness.getRoboticId());
-                    robotic.setTaskStatus(3);
-                    companyVoiceRoboticMapper.updateById(robotic);
-                }
-            }
-        }
+        super.postExecute(context, result);
+        super.updateWorkflowStatus(context.getWorkflowInstanceId(), ExecutionStatusEnum.SUCCESS);
+        super.taskFinish(context);
+//      //判定是否任务完成了更新任务的状态为执行完成
+//        CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
+//        if(null != roboticBusiness){
+//            Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
+//            if(Integer.valueOf(0).equals(i)){
+//                CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
+//                CompanyVoiceRobotic currentRobitic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticBusiness.getRoboticId());
+//                if(currentRobitic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+//                    robotic.setId(roboticBusiness.getRoboticId());
+//                    robotic.setTaskStatus(3);
+//                    companyVoiceRoboticMapper.updateById(robotic);
+//                }
+//            }
+//        }
     }
 }

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

@@ -35,6 +35,9 @@ public class WorkflowNodeFactory implements IWorkflowNodeFactory {
             case AI_SEND_MSG_TASK:
                 node = new AiSendMsgTaskNode(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;

+ 202 - 0
fs-service/src/main/java/com/fs/company/util/IpCheckUtil.java

@@ -0,0 +1,202 @@
+package com.fs.company.util;
+
+import cn.hutool.core.util.StrUtil;
+import com.fs.common.utils.IpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * IP校验工具类
+ * 用于校验请求来源IP是否在合法IP列表中
+ * 支持:精确IP匹配、通配符(*)匹配、CIDR格式(192.168.1.0/24)匹配
+ *
+ * @author MixLiu
+ */
+public class IpCheckUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(IpCheckUtil.class);
+
+    private IpCheckUtil() {
+        throw new AssertionError();
+    }
+
+    /**
+     * 校验请求来源IP是否在合法IP列表中(不合法则抛异常)
+     *
+     * @param request  HTTP请求
+     * @param legalIPs 逗号分隔的合法IP列表
+     * @throws SecurityException 当IP不合法时抛出
+     */
+    public static void checkRequestIpOrThrow(HttpServletRequest request, String legalIPs) {
+        if (!isRequestIpLegal(request, legalIPs)) {
+            String clientIp = IpUtil.getRequestIp(request);
+            log.warn("IP校验未通过, 来源IP: {}, 合法IP列表: {}", clientIp, legalIPs);
+            throw new SecurityException("非法IP来源: " + clientIp);
+        }
+    }
+
+    /**
+     * 校验请求来源IP是否在合法IP列表中
+     *
+     * @param request  HTTP请求
+     * @param legalIPs 逗号分隔的合法IP列表,支持精确IP、通配符(*)和CIDR格式
+     * @return true=合法, false=不合法
+     */
+    public static boolean isRequestIpLegal(HttpServletRequest request, String legalIPs) {
+        if (request == null) {
+            return false;
+        }
+        if (StrUtil.isBlank(legalIPs)) {
+            log.warn("legalIPs未配置,拒绝所有请求");
+            return false;
+        }
+        String clientIp = IpUtil.getRequestIp(request);
+        return isIpInList(clientIp, legalIPs);
+    }
+
+    /**
+     * 判断指定IP是否在IP列表中
+     *
+     * @param ip      待检查的IP (如 "192.168.1.100")
+     * @param ipList  逗号分隔的合法IP列表
+     *                <pre>
+     * 支持格式:
+     *   - 精确IP:      192.168.1.100
+     *   - 通配符:      192.168.1.* , 192.168.*.* , *
+     *   - CIDR:       192.168.1.0/24
+     *   - 混合逗号分隔: 192.168.1.100,10.0.0.*,172.16.0.0/12
+     *                </pre>
+     * @return true=在列表中
+     */
+    public static boolean isIpInList(String ip, String ipList) {
+        if (StrUtil.isBlank(ip) || StrUtil.isBlank(ipList)) {
+            return false;
+        }
+
+        String[] ipArr = ipList.split(",");
+        for (String allowedIp : ipArr) {
+            allowedIp = allowedIp.trim();
+            if (matchIp(ip, allowedIp)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 单个IP与单个规则匹配
+     *
+     * @param ip      待检查的IP
+     * @param pattern 匹配规则:精确IP / 通配符(*) / CIDR格式
+     */
+    public static boolean matchIp(String ip, String pattern) {
+        if (StrUtil.isBlank(ip) || StrUtil.isBlank(pattern)) {
+            return false;
+        }
+        pattern = pattern.trim();
+
+        // 通配所有
+        if ("*".equals(pattern)) {
+            return true;
+        }
+
+        // 精确匹配
+        if (ip.equals(pattern)) {
+            return true;
+        }
+
+        // CIDR格式匹配 (如 192.168.1.0/24)
+        if (pattern.contains("/")) {
+            return matchCidr(ip, pattern);
+        }
+
+        // 通配符匹配 (如 192.168.1.* 或 192.168.*)
+        if (pattern.contains("*")) {
+            return matchWildcard(ip, pattern);
+        }
+
+        return false;
+    }
+
+    /**
+     * 通配符匹配,如 192.168.1.* 或 192.168.*.* 或 192.*
+     */
+    private static boolean matchWildcard(String ip, String pattern) {
+        String[] ipParts = ip.split("\\.");
+        String[] patternParts = pattern.split("\\.");
+
+        // 如果模式段数不足4段,补全为通配符
+        if (patternParts.length < 4) {
+            String[] full = new String[4];
+            System.arraycopy(patternParts, 0, full, 0, patternParts.length);
+            for (int i = patternParts.length; i < 4; i++) {
+                full[i] = "*";
+            }
+            patternParts = full;
+        }
+
+        if (ipParts.length != 4) {
+            return false;
+        }
+
+        for (int i = 0; i < 4; i++) {
+            if ("*".equals(patternParts[i])) {
+                continue;
+            }
+            if (!ipParts[i].equals(patternParts[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * CIDR格式匹配,如 192.168.1.0/24
+     */
+    private static boolean matchCidr(String ip, String cidr) {
+        try {
+            String[] parts = cidr.split("/");
+            if (parts.length != 2) {
+                return false;
+            }
+            String subnet = parts[0];
+            int prefixLength = Integer.parseInt(parts[1]);
+
+            int ipInt = ipToInt(ip);
+            if (ipInt == -1) {
+                return false;
+            }
+
+            int subnetInt = ipToInt(subnet);
+            if (subnetInt == -1) {
+                return false;
+            }
+
+            int mask = prefixLength == 0 ? 0 : (0xFFFFFFFF << (32 - prefixLength));
+            return (ipInt & mask) == (subnetInt & mask);
+        } catch (Exception e) {
+            log.debug("CIDR匹配异常, ip={}, cidr={}", ip, cidr, e);
+            return false;
+        }
+    }
+
+    /**
+     * IP字符串转32位整数
+     */
+    private static int ipToInt(String ip) {
+        try {
+            String[] parts = ip.split("\\.");
+            if (parts.length != 4) {
+                return -1;
+            }
+            return (Integer.parseInt(parts[0]) << 24)
+                    | (Integer.parseInt(parts[1]) << 16)
+                    | (Integer.parseInt(parts[2]) << 8)
+                    | Integer.parseInt(parts[3]);
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+}

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

@@ -55,6 +55,8 @@ public class AiCallConfigVO {
      * 音色来源(音色列表接口返回的 voiceSource,如 aliyun_tts)
      */
     private String voiceSource;
+
+    private String ttsModels;
     /**
      * tts厂商 / 技能组 id(技能组列表接口返回的 groupId)
      */

+ 4 - 0
fs-service/src/main/java/com/fs/company/vo/CidConfigVO.java

@@ -10,4 +10,8 @@ public class CidConfigVO {
     private BigDecimal callCharge;
 
     private BigDecimal smsCharge;
+    /**
+     * 允许的IPs 逗号分隔的字符串
+     */
+    private String legalIPs;
 }

+ 43 - 0
fs-service/src/main/java/com/fs/company/vo/InboundCallInfo.java

@@ -0,0 +1,43 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/28 16:43
+ * @description
+ */
+@Data
+public class InboundCallInfo {
+
+    /**
+     * uuid
+     */
+    private String uuid;
+
+    /**
+     * 回调地址
+     */
+    private String callBackUrl;
+
+    /**
+     * 公司id
+     */
+    private Long fsCompanyId;
+
+    /**
+     * 呼入场景
+     */
+    private Integer fsSceneType;
+
+    /**
+     * 对话内容
+     */
+    private String chatContent;
+
+    /**
+     * 主叫号码
+     */
+    private String caller;
+
+}

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

@@ -33,4 +33,6 @@ public class EasyCallCreateTaskParam {
     private Double avgCallTalkTimeLen;
     /** 平均事后处理时长(taskType=0时必填,单位秒) */
     private Double avgCallEndProcessTimeLen;
+
+    private String ttsModels;
 }

+ 22 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java

@@ -4,6 +4,7 @@ import lombok.Data;
 import lombok.experimental.Accessors;
 
 import java.io.Serializable;
+import java.util.List;
 
 /**
  * 呼入大模型配置对象 cc_inbound_llm_account
@@ -72,4 +73,25 @@ public class EasyCallInboundLlmVO implements Serializable {
 
     /** AI转接分机号 */
     private String aiTransferExtNumber;
+    /**
+     * 公司可见的ID列表
+     */
+    private List<Long> visibleIds;
+
+    /**
+     * 公司id
+     */
+    private Long companyId;
+
+    /**
+     * 呼入回调地址
+     */
+    private String callBackUrl;
+
+    /**
+     * 场景类型
+     */
+    private Integer fsSceneType;
+
+
 }

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

@@ -48,7 +48,11 @@ public enum NodeTypeEnum {
     /**
      * AI企微添加个微
      */
-    AI_QW_ADD_WX_TASK("AI_QW_ADD_WX_TASK", "AI企微添加个微",9);
+    AI_QW_ADD_WX_TASK("AI_QW_ADD_WX_TASK", "AI企微添加个微",9),
+    /**
+     * AI添加微信(新)
+     */
+    AI_ADD_WX_TASK_NEW("AI_ADD_WX_TASK_NEW", "AI添加微信(新)", 10);
 
     private final String code;
     private final String description;

+ 18 - 23
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -489,7 +489,7 @@ public class AiHookServiceImpl implements AiHookService {
         // 添加脱敏逻辑
         if(qwExternalContacts.getType() == 1){
             FastGptChatSession fastGptChatSession= getFastGptSession(qwExternalContacts,user,dto);
-            if (qwContent.contains("我已经添加了你")){
+            if (qwContent.contains("验证请求") || qwContent.contains("联系人验证请求") || qwContent.contains("我已经添加了你")){
                 return R.ok();
             }
             if(type == 104||type == 101){
@@ -2217,28 +2217,23 @@ public class AiHookServiceImpl implements AiHookService {
                 saveQwUserMsg(fastGptChatSession,2,count,sendUser);
                 // 客服进行回复后就转人工10分钟
                 if(type == 1){
-                    Calendar calendar = Calendar.getInstance();
-                    calendar.add(Calendar.MINUTE, -3);
-                    Date lastTime = calendar.getTime();
-                    //10:00:00
-                    if(lastTime.after(fastGptChatSession.getCreateTime())) {
-                        Calendar oneDayAgo = Calendar.getInstance();
-                        oneDayAgo.add(Calendar.DAY_OF_MONTH, -1);
-                        // 增加判断:lastTime必须在createTime之后,且与当前时间相差不超过1天
-                        if (oneDayAgo.getTime().after(fastGptChatSession.getLastTime())) {
-                            Calendar calendar1 = Calendar.getInstance();
-                            //定时任务会处理10分钟以内的,所以设置20分钟
-                            calendar1.add(Calendar.MINUTE, 20);
-                            Date expireTime = calendar1.getTime();
-
-                            FastGptChatSession chatSession = new FastGptChatSession();
-                            chatSession.setLastTime(expireTime);
-                            chatSession.setIsArtificial(1);
-                            chatSession.setUserId(String.valueOf(sender));
-                            chatSession.setSessionId(fastGptChatSession.getSessionId());
-
-                            fastGptChatSessionMapper.updateFastGptChatSession(chatSession);
-                        }
+                    //如果有欢迎语,进线就会转人工
+                    Calendar oneDayAgo = Calendar.getInstance();
+                    oneDayAgo.add(Calendar.DAY_OF_MONTH, -1);
+                    // 增加判断:lastTime必须在createTime之后,且与当前时间相差不超过1天
+                    if (oneDayAgo.getTime().after(fastGptChatSession.getLastTime())) {
+                        Calendar calendar1 = Calendar.getInstance();
+                        //定时任务会处理10分钟以内的,所以设置20分钟
+                        calendar1.add(Calendar.MINUTE, 20);
+                        Date expireTime = calendar1.getTime();
+
+                        FastGptChatSession chatSession = new FastGptChatSession();
+                        chatSession.setLastTime(expireTime);
+                        chatSession.setIsArtificial(1);
+                        chatSession.setUserId(String.valueOf(sender));
+                        chatSession.setSessionId(fastGptChatSession.getSessionId());
+
+                        fastGptChatSessionMapper.updateFastGptChatSession(chatSession);
                     }
                 }
             }else {

+ 17 - 0
fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java

@@ -43,4 +43,21 @@ public class CidPhoneConfig implements Serializable {
      * 拨打次数
      * **/
     private Long numberCalls;
+
+    /**
+     * 是否允许重复客户
+     */
+    private Boolean allowRepeatCustomer;
+
+    /**
+     * 配置回调地址
+     */
+    private String callbackUrl;
+
+    /**
+     * 呼入回调地址
+     */
+    private String inboundCallbackUrl;
+
+
 }

+ 16 - 0
fs-service/src/main/java/com/fs/his/dto/SendResultDetailDTO.java

@@ -0,0 +1,16 @@
+package com.fs.his.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SendResultDetailDTO implements Serializable {
+    private boolean success;
+    private String failReason;
+    private Long sopLogId;
+}

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/vo/StoreOperMainVO.java

@@ -2,8 +2,10 @@ package com.fs.hisStore.vo;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 @Data
+@NoArgsConstructor
 @AllArgsConstructor
 public class StoreOperMainVO {
     private String id;

+ 20 - 0
fs-service/src/main/java/com/fs/ipad/vo/WxBaseVo.java

@@ -0,0 +1,20 @@
+package com.fs.ipad.vo;
+
+import lombok.Data;
+
+@Data
+public class WxBaseVo {
+
+    private Long id;
+    private Long serverId;
+    private String userRemark;
+    private String remark;
+
+
+    public void setBase(WxBaseVo vo){
+        this.id = vo.getId();
+        this.serverId = vo.getServerId();
+        this.remark = vo.getRemark();
+        this.userRemark = vo.getUserRemark();
+    }
+}

+ 9 - 0
fs-service/src/main/java/com/fs/ipad/vo/WxTxtVo.java

@@ -0,0 +1,9 @@
+package com.fs.ipad.vo;
+
+import lombok.Data;
+
+@Data
+public class WxTxtVo extends WxBaseVo {
+
+    private String content;
+}

+ 2 - 0
fs-service/src/main/java/com/fs/live/vo/DateRange.java

@@ -2,8 +2,10 @@ package com.fs.live.vo;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 @Data
+@NoArgsConstructor
 @AllArgsConstructor
 public class DateRange {
     private String start;

+ 11 - 4
fs-service/src/main/java/com/fs/wxcid/domain/WxContact.java

@@ -1,8 +1,11 @@
 package com.fs.wxcid.domain;
 
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntityTow;
 import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
 import lombok.EqualsAndHashCode;
 
 /**
@@ -14,10 +17,6 @@ import lombok.EqualsAndHashCode;
 @Data
 @EqualsAndHashCode(callSuper = true)
 public class WxContact extends BaseEntityTow {
-
-    /** crm客户ID */
-    private Long id;
-
     /** 微信ID */
     @Excel(name = "微信ID")
     private String userName;
@@ -71,6 +70,14 @@ public class WxContact extends BaseEntityTow {
     private Long companyUserId;
 
     private Long customerId;
+    // 是否是好友1是0否
+    private Integer friends;
+
+    /**
+     * crm 用户ID
+     */
+    @TableField(exist = false)
+    private Long crmUserId;
 
 
 }

+ 38 - 0
fs-service/src/main/java/com/fs/wxcid/dto/callback/WxCallbackVo.java

@@ -0,0 +1,38 @@
+package com.fs.wxcid.dto.callback;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fs.wxcid.dto.common.IntegerWrapper;
+import com.fs.wxcid.dto.common.StringWrapper;
+import lombok.Data;
+
+@Data
+public class WxCallbackVo {
+    private String key;
+    private Message message;
+    private String type;
+    @Data
+    public static class Message{
+        @JsonProperty("msg_id")
+        private Long msgId;
+        @JsonProperty("from_user_name")
+        private StringWrapper fromUserName;
+        @JsonProperty("to_user_name")
+        private StringWrapper toUserName;
+        @JsonProperty("msg_type")
+        private Integer msgType;
+        private StringWrapper content;
+        private Integer status;
+        @JsonProperty("img_status")
+        private Integer imgStatus;
+        @JsonProperty("img_buf")
+        private IntegerWrapper imgBuf;
+        @JsonProperty("create_time")
+        private Integer createTime;
+        @JsonProperty("msg_source")
+        private String msgSource;
+        @JsonProperty("push_content")
+        private String pushContent;
+        @JsonProperty("new_msg_id")
+        private Long newMsgId;
+    }
+}

+ 68 - 0
fs-service/src/main/java/com/fs/wxcid/service/IWxMsgLogService.java

@@ -0,0 +1,68 @@
+package com.fs.wxcid.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.wxcid.domain.WxMsgLog;
+import com.fs.wxcid.dto.callback.WxCallbackVo;
+import com.fs.wxcid.vo.wxvo.WxSendResultMsgVo;
+
+import java.util.List;
+
+/**
+ * 个微消息记录Service接口
+ * 
+ * @author fs
+ * @date 2025-12-16
+ */
+public interface IWxMsgLogService extends IService<WxMsgLog>{
+    /**
+     * 查询个微消息记录
+     * 
+     * @param id 个微消息记录主键
+     * @return 个微消息记录
+     */
+    WxMsgLog selectWxMsgLogById(Long id);
+
+    /**
+     * 查询个微消息记录列表
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 个微消息记录集合
+     */
+    List<WxMsgLog> selectWxMsgLogList(WxMsgLog wxMsgLog);
+
+    /**
+     * 新增个微消息记录
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 结果
+     */
+    int insertWxMsgLog(WxMsgLog wxMsgLog);
+
+    /**
+     * 修改个微消息记录
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 结果
+     */
+    int updateWxMsgLog(WxMsgLog wxMsgLog);
+
+    /**
+     * 批量删除个微消息记录
+     * 
+     * @param ids 需要删除的个微消息记录主键集合
+     * @return 结果
+     */
+    int deleteWxMsgLogByIds(Long[] ids);
+
+    /**
+     * 删除个微消息记录信息
+     * 
+     * @param id 个微消息记录主键
+     * @return 结果
+     */
+    int deleteWxMsgLogById(Long id);
+
+    void insertLog(WxCallbackVo callbackVo, CompanyWxAccount account, int type);
+    void insertLog(WxSendResultMsgVo vo, CompanyWxAccount account, int type);
+}

+ 131 - 0
fs-service/src/main/java/com/fs/wxcid/service/impl/WxMsgLogServiceImpl.java

@@ -0,0 +1,131 @@
+package com.fs.wxcid.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.wxcid.domain.WxMsgLog;
+import com.fs.wxcid.dto.callback.WxCallbackVo;
+import com.fs.wxcid.mapper.WxMsgLogMapper;
+import com.fs.wxcid.service.IWxMsgLogService;
+import com.fs.wxcid.vo.wxvo.WxSendResultMsgVo;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 个微消息记录Service业务层处理
+ * 
+ * @author fs
+ * @date 2025-12-16
+ */
+@Service
+public class WxMsgLogServiceImpl extends ServiceImpl<WxMsgLogMapper, WxMsgLog> implements IWxMsgLogService {
+
+    /**
+     * 查询个微消息记录
+     * 
+     * @param id 个微消息记录主键
+     * @return 个微消息记录
+     */
+    @Override
+    public WxMsgLog selectWxMsgLogById(Long id)
+    {
+        return baseMapper.selectWxMsgLogById(id);
+    }
+
+    /**
+     * 查询个微消息记录列表
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 个微消息记录
+     */
+    @Override
+    public List<WxMsgLog> selectWxMsgLogList(WxMsgLog wxMsgLog)
+    {
+        return baseMapper.selectWxMsgLogList(wxMsgLog);
+    }
+
+    /**
+     * 新增个微消息记录
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 结果
+     */
+    @Override
+    public int insertWxMsgLog(WxMsgLog wxMsgLog)
+    {
+        wxMsgLog.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertWxMsgLog(wxMsgLog);
+    }
+
+    /**
+     * 修改个微消息记录
+     * 
+     * @param wxMsgLog 个微消息记录
+     * @return 结果
+     */
+    @Override
+    public int updateWxMsgLog(WxMsgLog wxMsgLog)
+    {
+        wxMsgLog.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateWxMsgLog(wxMsgLog);
+    }
+
+    /**
+     * 批量删除个微消息记录
+     * 
+     * @param ids 需要删除的个微消息记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteWxMsgLogByIds(Long[] ids)
+    {
+        return baseMapper.deleteWxMsgLogByIds(ids);
+    }
+
+    /**
+     * 删除个微消息记录信息
+     * 
+     * @param id 个微消息记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteWxMsgLogById(Long id)
+    {
+        return baseMapper.deleteWxMsgLogById(id);
+    }
+
+    @Override
+    public void insertLog(WxCallbackVo callbackVo, CompanyWxAccount account, int type) {
+        String formUser = callbackVo.getMessage().getFromUserName().getStr();
+        WxMsgLog logs = new WxMsgLog();
+        logs.setAccountId(account.getId());
+        logs.setAuthKey(account.getAuthKey());
+        logs.setType(callbackVo.getType());
+        logs.setMsgId(callbackVo.getMessage().getMsgId());
+        logs.setFromUserName(formUser);
+        logs.setToUserName(callbackVo.getMessage().getToUserName().getStr());
+        logs.setMsgType(callbackVo.getMessage().getMsgType());
+        logs.setContent(callbackVo.getMessage().getContent().getStr());
+        logs.setImgStatus(callbackVo.getMessage().getImgStatus());
+        logs.setImgBuf(callbackVo.getMessage().getImgBuf().getLen());
+        logs.setPushContent(callbackVo.getMessage().getPushContent());
+        logs.setNewMsgId(callbackVo.getMessage().getNewMsgId());
+        logs.setReceiveType(type);
+        save(logs);
+    }
+
+    @Override
+    public void insertLog(WxSendResultMsgVo vo, CompanyWxAccount account, int type) {
+        WxMsgLog logs = new WxMsgLog();
+        logs.setAccountId(account.getId());
+        logs.setAuthKey(account.getAuthKey());
+        logs.setType(vo.getType()+"");
+        logs.setFromUserName(vo.getRemark());
+        logs.setToUserName(account.getWxNo());
+        logs.setMsgType(vo.getType());
+        logs.setContent(vo.getTxt());
+        logs.setReceiveType(type);
+        save(logs);
+    }
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wxcid/vo/wxvo/AddResultWxVo.java

@@ -0,0 +1,15 @@
+package com.fs.wxcid.vo.wxvo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AddResultWxVo {
+    private String remark;
+    private String userName;
+    private String wxid;
+    private String bizJson;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/wxcid/vo/wxvo/AddWxVo.java

@@ -0,0 +1,19 @@
+package com.fs.wxcid.vo.wxvo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AddWxVo {
+    // 备注
+    private String remark;
+    // 手机号
+    private String phone;
+    // 加微申请文案
+    private String applyMsg;
+    // 业务参数
+    private String bizJson;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/wxcid/vo/wxvo/ContactInfoVo.java

@@ -0,0 +1,18 @@
+package com.fs.wxcid.vo.wxvo;
+
+import lombok.Data;
+
+@Data
+public class ContactInfoVo {
+    // 微信昵称
+    private String nickName;
+    // 微信号
+    private String wxNo;
+    // 微信头像
+    private String img;
+    // 备注
+    private String remark;
+    // 地区
+    private String address;
+
+}

+ 16 - 0
fs-service/src/main/java/com/fs/wxcid/vo/wxvo/MsgResultVo.java

@@ -0,0 +1,16 @@
+package com.fs.wxcid.vo.wxvo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MsgResultVo {
+    // 备注
+    private String remark;
+    // 发送类型0文本
+    private int type;
+    private String txt;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/wxcid/vo/wxvo/SyncInfoVo.java

@@ -0,0 +1,16 @@
+package com.fs.wxcid.vo.wxvo;
+
+import lombok.Data;
+
+@Data
+public class SyncInfoVo {
+    // 微信号
+    private String no;
+    // 微信昵称
+    private String name;
+    // 微信头像
+    private String img;
+    // 手机号
+    private String phone;
+
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wxcid/vo/wxvo/WxSendMsgVo.java

@@ -0,0 +1,15 @@
+package com.fs.wxcid.vo.wxvo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxSendMsgVo {
+    // 备注
+    private String wxId;
+    private String remark;
+    private String txt;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wxcid/vo/wxvo/WxSendResultMsgVo.java

@@ -0,0 +1,15 @@
+package com.fs.wxcid.vo.wxvo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class WxSendResultMsgVo {
+    // 备注
+    private String remark;
+    private int type;
+    private String txt;
+}

+ 58 - 0
fs-service/src/main/java/com/fs/wxwork/service/WxIpadService.java

@@ -0,0 +1,58 @@
+package com.fs.wxwork.service;
+
+import com.alibaba.fastjson.TypeReference;
+import com.fs.common.core.domain.ResponseResult;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
+import com.fs.company.param.AddWxActionParam;
+import com.fs.ipad.vo.WxTxtVo;
+import com.fs.wxcid.domain.CidIpadServer;
+import com.fs.wxcid.service.ICidIpadServerService;
+import com.fs.wxcid.vo.wxvo.WxSendMsgVo;
+import com.fs.wxwork.utils.WxHttpUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.TimeUnit;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+public class WxIpadService {
+    /**
+     * 基础地址
+     */
+    private final RedisCache redisCache;
+    private final ICidIpadServerService cidIpadServerService;
+
+    public String getUrl(Long serverId) {
+        String url = redisCache.getCacheObject("serverId:" + serverId);
+        if (url != null && !url.isEmpty()) {
+            return url;
+        }
+        log.info("serverId:" + serverId);
+        CidIpadServer qwIpadServer = cidIpadServerService.getById(serverId);
+        if (qwIpadServer == null||qwIpadServer.getUrl()==null) {
+            throw new CustomException("未获取到服务地址与端口");
+        }
+        redisCache.setCacheObject("cid:serverId:" + serverId,qwIpadServer.getUrl(),2, TimeUnit.HOURS);
+        return qwIpadServer.getUrl();
+    }
+
+    public void sendTxt(WxTxtVo vo){
+        String url = getUrl(vo.getServerId());
+        WxSendMsgVo msgVO = new WxSendMsgVo();
+        msgVO.setWxId(vo.getRemark());
+        msgVO.setRemark(vo.getUserRemark());
+        msgVO.setTxt(vo.getContent());
+        ResponseResult<Void> result = WxHttpUtil.postWithType(url+"/app/common/sendMsg", msgVO, new TypeReference<ResponseResult<Void>>() {
+        }, vo.getServerId());
+        log.info("发送结果:{}", result);
+    }
+    public void addWx(AddWxActionParam vo){
+        String url = getUrl(vo.getServerId());
+        ResponseResult<Void> result = WxHttpUtil.postWithType(url+"/app/common/addWxAction", vo, new TypeReference<ResponseResult<Void>>() {
+        }, vo.getServerId());
+    }
+}

+ 307 - 0
fs-service/src/main/java/com/fs/wxwork/utils/WxHttpUtil.java

@@ -0,0 +1,307 @@
+package com.fs.wxwork.utils;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.TypeReference;
+import com.fs.common.exception.base.BaseException;
+import com.fs.ipad.enums.ErrCodeEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * HTTP请求工具类(基于Hutool封装)
+ * 增强功能:请求参数、Header和响应日志记录
+ */
+@Slf4j
+public class WxHttpUtil {
+    private static final Logger logger = LoggerFactory.getLogger(WxHttpUtil.class);
+
+    /**
+     * 发送GET请求
+     * @param url 请求URL
+     * @return 响应字符串
+     */
+    public static String get(String url) {
+        return get(url, null, null);
+    }
+
+    /**
+     * 发送带参数的GET请求
+     * @param url 请求URL
+     * @param params 请求参数
+     * @return 响应字符串
+     */
+    public static String get(String url, Map<String, Object> params) {
+        return get(url, params, null);
+    }
+
+    /**
+     * 发送带参数和Header的GET请求
+     * @param url 请求URL
+     * @param params 请求参数
+     * @param headers 请求头
+     * @return 响应字符串
+     */
+    public static String get(String url, Map<String, Object> params, Map<String, String> headers) {
+        // 记录请求信息
+        logRequest("GET", url, params, null, headers);
+
+        // 构建请求
+        HttpRequest request = HttpRequest.get(url);
+
+        // 添加参数
+        if (MapUtil.isNotEmpty(params)) {
+            request.form(params);
+        }
+
+        // 添加请求头
+        if (MapUtil.isNotEmpty(headers)) {
+            request.addHeaders(headers);
+        }
+
+        // 执行请求并获取响应
+        HttpResponse response = request.execute();
+        String responseBody = response.body();
+
+        // 记录响应信息
+        logResponse(response, responseBody);
+
+        return responseBody;
+    }
+    /**
+     * 发送POST请求(JSON格式)
+     *
+     * @param url      请求URL
+     * @param jsonBody JSON请求体
+     * @param serverId
+     * @return 响应字符串
+     */
+    public static <T> T postWithType(String url, Object jsonBody, TypeReference<T> type, Long serverId) {
+        String json = JSON.toJSONString(jsonBody);
+        String post = post(url, json, null, serverId);
+        log.info("请求服务器地址:{},请求参数:{},返回数据:{},请求时间:{}", url, json, post, LocalDateTime.now());
+        JSONObject jsonObject = JSON.parseObject(post);
+        if(jsonObject.getInteger("code") != 200){
+            log.error("请求服务器地址:{},请求参数:{},返回数据:{},请求时间:{}", url, json, JSON.toJSONString(jsonObject), LocalDateTime.now());
+            String errMsg = ErrCodeEnum.getErrMsg(jsonObject.getInteger("code"));
+            if("未知的服务端错误".equals(errMsg)){
+                errMsg = jsonObject.getString("msg");
+            }
+            throw new BaseException(errMsg);
+        }
+        return JSON.parseObject(post, type);
+    }
+    /**
+     * 发送带Header的POST请求(JSON格式)
+     * @param url 请求URL
+     * @param jsonBody JSON请求体
+     * @param headers 请求头
+     * @return 响应字符串
+     */
+    public static String post(String url, String jsonBody, Map<String, String> headers, Long serviceId) {
+        // 记录请求信息
+        logRequest("POST", url, null, jsonBody, headers);
+
+        // 构建请求
+        HttpRequest request = HttpRequest.post(url)
+                .body(jsonBody);
+
+        // 添加请求头
+        if (MapUtil.isNotEmpty(headers)) {
+            request.addHeaders(headers);
+        }
+
+        // 执行请求并获取响应
+        HttpResponse response;
+        try {
+            response = request.execute();
+        }catch (IORuntimeException e){
+            return "{\"data\":null,\"errcode\":400,\"errmsg\":\"连接超时,服务器宕机 - serviceId: " + serviceId +"\"}";
+        }
+        String responseBody = response.body();
+
+        // 记录响应信息
+        logResponse(response, responseBody);
+
+        return responseBody;
+    }
+
+    /**
+     * 发送表单POST请求
+     * @param url 请求URL
+     * @param formParams 表单参数
+     * @return 响应字符串
+     */
+    public static String postForm(String url, Map<String, Object> formParams) {
+        return postForm(url, formParams, null);
+    }
+
+    /**
+     * 发送带Header的表单POST请求
+     * @param url 请求URL
+     * @param formParams 表单参数
+     * @param headers 请求头
+     * @return 响应字符串
+     */
+    public static String postForm(String url, Map<String, Object> formParams, Map<String, String> headers) {
+        // 记录请求信息
+        logRequest("POST", url, formParams, null, headers);
+
+        // 构建请求
+        HttpRequest request = HttpRequest.post(url);
+
+        // 添加表单参数
+        if (MapUtil.isNotEmpty(formParams)) {
+            request.form(formParams);
+        }
+
+        // 添加请求头
+        if (MapUtil.isNotEmpty(headers)) {
+            request.addHeaders(headers);
+        }
+
+        // 执行请求并获取响应
+        HttpResponse response = request.execute();
+        String responseBody = response.body();
+
+        // 记录响应信息
+        logResponse(response, responseBody);
+
+        return responseBody;
+    }
+
+    /**
+     * 发送PUT请求(JSON格式)
+     * @param url 请求URL
+     * @param jsonBody JSON请求体
+     * @return 响应字符串
+     */
+    public static String put(String url, String jsonBody) {
+        return put(url, jsonBody, null);
+    }
+
+    /**
+     * 发送带Header的PUT请求(JSON格式)
+     * @param url 请求URL
+     * @param jsonBody JSON请求体
+     * @param headers 请求头
+     * @return 响应字符串
+     */
+    public static String put(String url, String jsonBody, Map<String, String> headers) {
+        // 记录请求信息
+        logRequest("PUT", url, null, jsonBody, headers);
+
+        // 构建请求
+        HttpRequest request = HttpRequest.put(url)
+                .body(jsonBody);
+
+        // 添加请求头
+        if (MapUtil.isNotEmpty(headers)) {
+            request.addHeaders(headers);
+        }
+
+        // 执行请求并获取响应
+        HttpResponse response = request.execute();
+        String responseBody = response.body();
+
+        // 记录响应信息
+        logResponse(response, responseBody);
+
+        return responseBody;
+    }
+
+    /**
+     * 发送DELETE请求
+     * @param url 请求URL
+     * @return 响应字符串
+     */
+    public static String delete(String url) {
+        return delete(url, null);
+    }
+
+    /**
+     * 发送带Header的DELETE请求
+     * @param url 请求URL
+     * @param headers 请求头
+     * @return 响应字符串
+     */
+    public static String delete(String url, Map<String, String> headers) {
+        // 记录请求信息
+        logRequest("DELETE", url, null, null, headers);
+
+        // 构建请求
+        HttpRequest request = HttpRequest.delete(url);
+
+        // 添加请求头
+        if (MapUtil.isNotEmpty(headers)) {
+            request.addHeaders(headers);
+        }
+
+        // 执行请求并获取响应
+        HttpResponse response = request.execute();
+        String responseBody = response.body();
+
+        // 记录响应信息
+        logResponse(response, responseBody);
+
+        return responseBody;
+    }
+
+    /**
+     * 记录请求信息
+     * @param method 请求方法
+     * @param url 请求URL
+     * @param params 请求参数
+     * @param body 请求体
+     * @param headers 请求头
+     */
+    private static void logRequest(String method, String url, Map<String, Object> params, String body, Map<String, String> headers) {
+        logger.debug("发送请求: {} {}", method, url);
+
+        if (MapUtil.isNotEmpty(params)) {
+            logger.debug("请求参数: {}", JSONUtil.toJsonStr(params));
+        }
+
+        if (body != null && !body.isEmpty()) {
+            // 如果是较长的JSON,可能需要截断
+            String logBody = body.length() > 1000 ? body.substring(0, 1000) + "... (截断)" : body;
+            logger.debug("请求体: {}", logBody);
+        }
+
+        if (MapUtil.isNotEmpty(headers)) {
+            logger.debug("请求头: {}", JSONUtil.toJsonStr(headers));
+        }
+    }
+
+    /**
+     * 记录响应信息
+     * @param response 响应对象
+     * @param responseBody 响应体
+     */
+    private static void logResponse(HttpResponse response, String responseBody) {
+        logger.debug("响应状态码: {}", response.getStatus());
+
+        // 记录响应头
+        Map<String, List<String>> headers = response.headers();
+        if (MapUtil.isNotEmpty(headers)) {
+            logger.debug("响应头: {}", JSONUtil.toJsonStr(headers));
+        }
+
+        // 记录响应体,但限制大小以防止日志过大
+        if (responseBody != null && !responseBody.isEmpty()) {
+            String logBody = responseBody.length() > 1000 ?
+                    responseBody.substring(0, 1000) + "... (截断)" : responseBody;
+            logger.debug("响应体: {}", logBody);
+        }
+    }
+}

+ 4 - 4
fs-service/src/main/resources/application-dev.yml

@@ -7,15 +7,15 @@ spring:
     # redis 配置
     redis:
         # 地址
-#        host: localhost
-        host: 192.168.0.245
+        host: localhost
+#        host: 192.168.0.245
         # 端口,默认为6379
         port: 6379
         # 数据库索引
         database: 0
         # 密码
-#        password:
-        password: Ylrztek250218!3@.
+        password:
+#        password: Ylrztek250218!3@.
         # 连接超时时间
         timeout: 20s
         lettuce:

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

@@ -258,4 +258,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             )
         </foreach>
     </insert>
+
+    <select id="selectWxContectByWorkflowInstanceId" resultType="com.fs.wxcid.domain.WxContact">
+        SELECT
+            t3.user_id as crmUserId,
+            t3.user_name as remark,
+            t3.phone as phone,
+            t2.wx_client_id as wxClientId,
+            t4.account_id as accountId,
+            t4.company_id as companyId,
+            t4.company_user_id as companyUserId
+        FROM
+            company_ai_workflow_exec t1
+                left join company_voice_robotic_business t2 on t1.business_key = t2.id
+                left join company_voice_robotic_callees t3 on t3.id  =  t2.callee_id
+                left join company_wx_client t4 on t4.id = t2.wx_client_id
+        WHERE
+            t1.workflow_instance_id = #{workflowInstanceId}
+    </select>
+
+    <select id="selectWxClientIdByWorkflowInstanceId" resultType="java.lang.Long" >
+        select
+            t2.wx_client_id
+        from company_ai_workflow_exec t1
+                 inner join company_voice_robotic_business t2 on t1.business_key = t2.id
+        where t1.workflow_instance_id = #{workflowInstanceId}
+    </select>
 </mapper>

+ 77 - 0
fs-service/src/main/resources/mapper/company/CompanyInboundBindMapper.xml

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyInboundBindMapper">
+    
+    <resultMap type="CompanyInboundBind" id="CompanyInboundBindResult">
+        <result property="id"    column="id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="inboundLlmAccountId"    column="inbound_llm_account_id"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectCompanyInboundBindVo">
+        select id, company_id, inbound_llm_account_id, create_time from company_inbound_bind
+    </sql>
+
+    <select id="selectCompanyInboundBindList" parameterType="CompanyInboundBind" resultMap="CompanyInboundBindResult">
+        <include refid="selectCompanyInboundBindVo"/>
+        <where>  
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="inboundLlmAccountId != null "> and inbound_llm_account_id = #{inboundLlmAccountId}</if>
+        </where>
+    </select>
+    
+    <select id="selectCompanyInboundBindById" parameterType="Long" resultMap="CompanyInboundBindResult">
+        <include refid="selectCompanyInboundBindVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertCompanyInboundBind" parameterType="CompanyInboundBind" useGeneratedKeys="true" keyProperty="id">
+        insert into company_inbound_bind
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">company_id,</if>
+            <if test="inboundLlmAccountId != null">inbound_llm_account_id,</if>
+            <if test="createTime != null">create_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">#{companyId},</if>
+            <if test="inboundLlmAccountId != null">#{inboundLlmAccountId},</if>
+            <if test="createTime != null">#{createTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateCompanyInboundBind" parameterType="CompanyInboundBind">
+        update company_inbound_bind
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="inboundLlmAccountId != null">inbound_llm_account_id = #{inboundLlmAccountId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCompanyInboundBindById" parameterType="Long">
+        delete from company_inbound_bind where id = #{id}
+    </delete>
+
+    <delete id="deleteCompanyInboundBindByIds" parameterType="String">
+        delete from company_inbound_bind where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectIdsByCompanyId" resultType="CompanyInboundBind">
+        select * from company_inbound_bind where company_id = #{companyId}
+    </select>
+
+    <delete id="deleteByCompanyIdAndInboundLlmAccountIds">
+        delete from company_inbound_bind where company_id = #{companyId}
+              and  inbound_llm_account_id in
+        <foreach item="id" collection="inboundLlmAccountIds" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 79 - 0
fs-service/src/main/resources/mapper/company/CompanySiptaskInfoMapper.xml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanySiptaskInfoMapper">
+    
+    <resultMap type="CompanySiptaskInfo" id="CompanySiptaskInfoResult">
+        <result property="id"    column="id"    />
+        <result property="workflowId"    column="workflow_id"    />
+        <result property="taskId"    column="task_id"    />
+        <result property="nodeKey"    column="node_key"    />
+        <result property="batchId"    column="batch_id"    />
+        <result property="taskJson"    column="task_json"    />
+    </resultMap>
+
+    <sql id="selectCompanySiptaskInfoVo">
+        select id, workflow_id, task_id, node_key, batch_id, task_json from company_siptask_info
+    </sql>
+
+    <select id="selectCompanySiptaskInfoList" parameterType="CompanySiptaskInfo" resultMap="CompanySiptaskInfoResult">
+        <include refid="selectCompanySiptaskInfoVo"/>
+        <where>  
+            <if test="workflowId != null "> and workflow_id = #{workflowId}</if>
+            <if test="taskId != null "> and task_id = #{taskId}</if>
+            <if test="nodeKey != null  and nodeKey != ''"> and node_key = #{nodeKey}</if>
+            <if test="batchId != null "> and batch_id = #{batchId}</if>
+            <if test="taskJson != null  and taskJson != ''"> and task_json = #{taskJson}</if>
+        </where>
+    </select>
+    
+    <select id="selectCompanySiptaskInfoById" parameterType="Long" resultMap="CompanySiptaskInfoResult">
+        <include refid="selectCompanySiptaskInfoVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertCompanySiptaskInfo" parameterType="CompanySiptaskInfo" useGeneratedKeys="true" keyProperty="id">
+        insert into company_siptask_info
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="workflowId != null">workflow_id,</if>
+            <if test="taskId != null">task_id,</if>
+            <if test="nodeKey != null">node_key,</if>
+            <if test="batchId != null">batch_id,</if>
+            <if test="taskJson != null">task_json,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="workflowId != null">#{workflowId},</if>
+            <if test="taskId != null">#{taskId},</if>
+            <if test="nodeKey != null">#{nodeKey},</if>
+            <if test="batchId != null">#{batchId},</if>
+            <if test="taskJson != null">#{taskJson},</if>
+         </trim>
+    </insert>
+
+    <update id="updateCompanySiptaskInfo" parameterType="CompanySiptaskInfo">
+        update company_siptask_info
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="workflowId != null">workflow_id = #{workflowId},</if>
+            <if test="taskId != null">task_id = #{taskId},</if>
+            <if test="nodeKey != null">node_key = #{nodeKey},</if>
+            <if test="batchId != null">batch_id = #{batchId},</if>
+            <if test="taskJson != null">task_json = #{taskJson},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCompanySiptaskInfoById" parameterType="Long">
+        delete from company_siptask_info where id = #{id}
+    </delete>
+
+    <delete id="deleteCompanySiptaskInfoByIds" parameterType="String">
+        delete from company_siptask_info where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+    <select id="selectSipTaskInfoByTaskIdAndNodeKey" resultType="com.fs.company.domain.CompanySiptaskInfo">
+        select * from company_siptask_info where task_id=#{taskId} and node_key=#{nodeKey} limit 1
+    </select>
+</mapper>

+ 100 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceCloneRefMapper.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyVoiceCloneRefMapper">
+
+    <resultMap id="CompanyVoiceCloneRefResult" type="com.fs.company.domain.CompanyVoiceCloneRef">
+        <id property="id" column="id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="companyUserId" column="company_user_id"/>
+        <result property="voiceName" column="voice_name"/>
+        <result property="voiceCode" column="voice_code"/>
+        <result property="ttsId" column="tts_id"/>
+        <result property="status" column="status"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <select id="selectByCompanyIdAndTtsId" resultMap="CompanyVoiceCloneRefResult">
+        select id,voice_name,voice_code, company_id,company_user_id, tts_id, status, create_by, create_time, update_by, update_time, remark
+        from company_voice_clone_ref
+        where company_id = #{companyId}
+          and company_user_id = #{companyUserId}
+          and tts_id = #{ttsId}
+            limit 1
+    </select>
+    <select id="selectByCompanyIdAndCompanyUserId" resultType="java.lang.Long">
+        select tts_id
+        from company_voice_clone_ref
+        where company_id = #{companyId}
+          and company_user_id = #{companyUserId}
+            limit 1
+    </select>
+
+    <sql id="selectCcTtsAliyunVo">
+        select id, voice_name, voice_code, voice_enabled, voice_source, priority, provider,tts_models from cc_tts_aliyun
+    </sql>
+
+    <select id="selectCcTtsAliyunList" resultType="com.fs.aicall.domain.CcTtsAliyun">
+        <include refid="selectCcTtsAliyunVo"/>
+        <where>
+            <if test="voiceName != null  and voiceName != ''"> and voice_name like concat('%', #{voiceName}, '%')</if>
+            <if test="voiceCode != null  and voiceCode != ''"> and voice_code = #{voiceCode}</if>
+            <if test="voiceEnabled != null "> and voice_enabled = #{voiceEnabled}</if>
+            <if test="voiceSource != null  and voiceSource != ''"> and voice_source = #{voiceSource}</if>
+            <if test="priority != null "> and priority = #{priority}</if>
+            <if test="provider != null  and provider != ''"> and provider = #{provider}</if>
+        </where>
+        order by priority asc
+    </select>
+
+
+
+    <insert id="insertCompanyVoiceCloneRef" parameterType="com.fs.company.domain.CompanyVoiceCloneRef"
+            useGeneratedKeys="true" keyProperty="id">
+        insert into company_voice_clone_ref
+        (
+            voice_name,
+            voice_code,
+            company_id,
+            company_user_id,
+            tts_id,
+            status,
+            create_by,
+            create_time,
+            update_by,
+            update_time,
+            remark
+        )
+        values
+            (
+                #{voiceName},
+                #{voiceCode},
+                #{companyId},
+                #{companyUserId},
+                #{ttsId},
+                #{status},
+                #{createBy},
+                now(),
+                #{updateBy},
+                now(),
+                #{remark}
+            )
+    </insert>
+
+    <update id="updateCompanyVoiceCloneRef" parameterType="com.fs.company.domain.CompanyVoiceCloneRef">
+        update company_voice_clone_ref
+        <set>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            update_time = now()
+        </set>
+        where id = #{id}
+    </update>
+
+</mapper>

+ 55 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogAddwxMapper.xml

@@ -204,4 +204,59 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="roboticId != null"> and t1.robotic_id = #{roboticId}</if>
         </where>
     </select>
+
+    <select id="selectCompanyVoiceRoboticCallLogAddwxGroupList" resultType="com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx" parameterType="com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx">
+        select
+        robotic_id,
+        cvr.name as robotic_name,
+        count(1) as totalRecordCount,
+        sum(case when status = 1 then 1 else 0 end) as runningCount,
+        sum(case when status = 2 then 1 else 0 end) as successCount,
+        sum(case when status = 3 then 1 else 0 end) as failCount,
+        concat(round(sum(case when t3.is_add = 1 then 1 else 0 end) * 100.0 / count(1), 2), '%') as successRate
+        from company_voice_robotic_call_log_addwx t1
+        inner join company_voice_robotic cvr on cvr.id = t1.robotic_id
+        left join company_wx_client t3 on t3.id = t1.wx_client_id
+        <where>
+            <if test="roboticId != null">and robotic_id = #{roboticId}</if>
+        </where>
+        group by robotic_id
+    </select>
+
+
+    <select id="listByRoboticId" resultType="com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO" parameterType="com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx">
+        SELECT
+        t1.*,
+        t2.company_name,
+        t3.nick_name as companyUserName,
+        cvr.name as robotic_name
+        FROM company_voice_robotic_call_log_addwx t1
+        left join company t2 on t1.company_id = t2.company_id
+        left join company_user t3 on t3.user_id = t1.company_user_id
+        left join company_voice_robotic cvr on cvr.id = t1.robotic_id
+        where 1=1
+        <if test="roboticId != null">and t1.robotic_id = #{roboticId}</if>
+        <if test="wxClientId != null">and t1.wx_client_id = #{wxClientId}</if>
+        <if test="wxClientIds != null and wxClientIds.size() > 0">
+            AND t1.wx_client_id IN
+            <foreach collection='wxClientIds' item='item' open='(' separator=',' close=')'>
+                #{item}
+            </foreach>
+        </if>
+        <if test="wxAccountId != null and wxAccountId != ''">
+            and t1.wx_account_id like concat('%', #{wxAccountId}, '%')
+        </if>
+
+    </select>
+
+    <select id="selectCompanyVoiceRoboticAddwxLogCount" resultType="com.fs.company.vo.CompanyVoiceRoboticCallLogCount">
+        select
+            count(1) as recordCount,
+            sum(case when status = 2 then 1 else 0 end) as successRecordCount,
+            sum(case when run_time &gt;= CURDATE() and run_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY) then 1 else 0 end) as todayCount,
+            sum(case when status = 2 and run_time &gt;= CURDATE() and run_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY) then 1 else 0 end) as todaySuccessCount
+        from company_voice_robotic_call_log_addwx cw
+                 inner join company_voice_robotic cvr on cvr.id = cw.robotic_id
+    </select>
+
 </mapper>

+ 3 - 2
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml

@@ -206,7 +206,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         count(1) as totalRecordCount,
         sum(case when status = 1 then 1 else 0 end) as runningCount,
         sum(case when status = 2 then 1 else 0 end) as successCount,
-        sum(case when status = 3 then 1 else 0 end) as failCount
+        sum(case when status = 3 then 1 else 0 end) as failCount,
+        concat(round(sum(case when call_time is not null then 1 else 0 end) * 100.0 / count(1), 2), '%') as successRate
         from company_voice_robotic_call_log_callphone t1
         inner join company_voice_robotic cvr on cvr.id = t1.robotic_id
         <where>
@@ -225,7 +226,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </select>
 
 
-    <select id="listByRoboticId" resultType="com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO" parameterType="CompanyVoiceRoboticCallLogCallphone">
+    <select id="listByRoboticId" resultType="com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO" parameterType="com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone">
         SELECT
         t1.*,
         t2.company_name,

+ 3 - 1
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogSendmsgMapper.xml

@@ -48,9 +48,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         count(1) as totalRecordCount,
         sum(case when status = 1 then 1 else 0 end) as runningCount,
         sum(case when status = 2 then 1 else 0 end) as successCount,
-        sum(case when status = 3 then 1 else 0 end) as failCount
+        sum(case when status = 3 then 1 else 0 end) as failCount,
+        concat(round(ifnull(sum(case when cc.is_send_msg = 1 then 1 else 0 end) * 100.0 / nullif(count(1), 0),0),2),'%') as successRate
         from company_voice_robotic_call_log_sendmsg msg
         inner join company_voice_robotic cvr on cvr.id = msg.robotic_id
+        left join company_voice_robotic_callees cc on cc.id = msg.caller_id
         <where>
             <if test="roboticId != null">and robotic_id = #{roboticId}</if>
             <if test="callerId != null">and caller_id = #{callerId}</if>

+ 4 - 0
fs-service/src/main/resources/mapper/company/CompanyWxAccountMapper.xml

@@ -104,4 +104,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectByCompanyUserAndWxNo" resultType="com.fs.company.domain.CompanyWxAccount">
         select * from company_wx_account where company_user_id = #{userId} and wx_no = #{wxNo}
     </select>
+    <select id="selectCompanyWxAccountByWxRemark" resultType="com.fs.company.domain.CompanyWxAccount">
+        select * from company_wx_account where wx_remark like concat( #{wxRemark}, '%') limit 1
+    </select>
+
 </mapper>

+ 4 - 0
fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml

@@ -231,4 +231,8 @@
         where 1=1
         <if test="customerId != null">  and customer_id = #{customerId} </if>
     </select>
+
+    <select id="selectWxV2" resultType="com.fs.company.domain.CompanyWxClient">
+        select * from company_wx_client where account_id = #{id} and phone = #{phone}
+    </select>
 </mapper>

+ 323 - 0
fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml

@@ -0,0 +1,323 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.EasyCallInboundLlmMapper">
+
+    <resultMap id="InboundLlmResult" type="com.fs.company.vo.easycall.EasyCallInboundLlmVO">
+        <id property="id" column="id"/>
+        <result property="llmAccountId" column="llm_account_id"/>
+        <result property="inboundAlias" column="inbound_alias"/>
+        <result property="callee" column="callee"/>
+        <result property="voiceCode" column="voice_code"/>
+        <result property="voiceSource" column="voice_source"/>
+        <result property="serviceType" column="service_type"/>
+        <result property="asrProvider" column="asr_provider"/>
+        <result property="aiTransferType" column="ai_transfer_type"/>
+        <result property="aiTransferData" column="ai_transfer_data"/>
+        <result property="ivrId" column="ivr_id"/>
+        <result property="satisfSurveyIvrId" column="satisf_survey_ivr_id"/>
+    </resultMap>
+
+    <resultMap id="LlmAccountResult" type="com.fs.company.vo.easycall.EasyCallLlmAccountVO">
+        <id property="id" column="id"/>
+        <result property="name" column="name"/>
+        <result property="accountJson" column="account_json"/>
+        <result property="accountEntity" column="account_entity"/>
+        <result property="providerClassName" column="provider_class_name"/>
+    </resultMap>
+
+    <resultMap id="VoiceCodeResult" type="com.fs.company.vo.easycall.EasyCallVoiceCodeVO">
+        <result property="voiceCode" column="voice_code"/>
+        <result property="voiceName" column="voice_name"/>
+        <result property="voiceSource" column="voice_source"/>
+    </resultMap>
+
+    <resultMap id="BizGroupResult" type="com.fs.company.vo.easycall.EasyCallBizGroupVO">
+        <result property="groupId" column="group_id"/>
+        <result property="bizGroupName" column="biz_group_name"/>
+    </resultMap>
+
+    <resultMap id="GatewayResult" type="com.fs.company.vo.easycall.EasyCallGatewayVO">
+        <result property="id" column="id"/>
+        <result property="gwName" column="gw_name"/>
+        <result property="profileName" column="profile_name"/>
+        <result property="caller" column="caller"/>
+        <result property="calleePrefix" column="callee_prefix"/>
+        <result property="gwAddr" column="gw_addr"/>
+        <result property="codec" column="codec"/>
+        <result property="gwDesc" column="gw_desc"/>
+        <result property="purpose" column="purpose"/>
+    </resultMap>
+
+    <resultMap id="IvrResult" type="com.fs.company.vo.easycall.EasyCallIvrVO">
+        <result property="id" column="id"/>
+        <result property="ivrNodeName" column="node_name"/>
+    </resultMap>
+
+    <sql id="selectInboundLlmVo">
+        select id, llm_account_id, callee, voice_code, voice_source, service_type, asr_provider, ai_transfer_type, ai_transfer_data, ivr_id, satisf_survey_ivr_id, inbound_alias,call_back_url,fs_scene_type from cc_inbound_llm_account
+    </sql>
+
+    <select id="selectInboundLlmList" parameterType="com.fs.company.vo.easycall.EasyCallInboundLlmVO" resultMap="InboundLlmResult">
+        <include refid="selectInboundLlmVo"/>
+        <where>
+            <if test="llmAccountId != null">and llm_account_id = #{llmAccountId}</if>
+            <if test="callee != null and callee != ''">and callee = #{callee}</if>
+            <if test="inboundAlias != null and inboundAlias != ''">and inbound_alias = #{inboundAlias}</if>
+            <if test="voiceCode != null and voiceCode != ''">and voice_code = #{voiceCode}</if>
+            <if test="voiceSource != null and voiceSource != ''">and voice_source = #{voiceSource}</if>
+            <if test="serviceType != null and serviceType != ''">and service_type = #{serviceType}</if>
+            <if test="asrProvider != null and asrProvider != ''">and asr_provider = #{asrProvider}</if>
+            <if test="aiTransferType != null and aiTransferType != ''">and ai_transfer_type = #{aiTransferType}</if>
+            <if test="aiTransferData != null and aiTransferData != ''">and ai_transfer_data = #{aiTransferData}</if>
+            <if test="visibleIds != null ">
+                <if test="visibleIds.size() > 0">
+                    and id in
+                    <foreach item="item" collection="visibleIds" index="index" separator="," open="(" close=")">
+                        #{item}
+                    </foreach>
+                </if>
+            </if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectInboundLlmById" parameterType="Integer" resultMap="InboundLlmResult">
+        <include refid="selectInboundLlmVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectInboundLlmByCallee" parameterType="String" resultMap="InboundLlmResult">
+        <include refid="selectInboundLlmVo"/>
+        where callee = #{callee}
+    </select>
+
+    <insert id="insertInboundLlm" parameterType="com.fs.company.vo.easycall.EasyCallInboundLlmVO" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
+        insert into cc_inbound_llm_account
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="id != null">id,</if>
+            <if test="llmAccountId != null">llm_account_id,</if>
+            <if test="callee != null and callee != ''">callee,</if>
+            <if test="inboundAlias != null and inboundAlias != ''">inbound_alias,</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code,</if>
+            <if test="voiceSource != null and voiceSource != ''">voice_source,</if>
+            <if test="serviceType != null and serviceType != ''">service_type,</if>
+            <if test="asrProvider != null and asrProvider != ''">asr_provider,</if>
+            <if test="aiTransferType != null and aiTransferType != ''">ai_transfer_type,</if>
+            <if test="aiTransferData != null and aiTransferData != ''">ai_transfer_data,</if>
+            <if test="ivrId != null">ivr_id,</if>
+            <if test="satisfSurveyIvrId != null">satisf_survey_ivr_id,</if>
+            <if test="companyId != null">fs_company_id,</if>
+            <if test="callBackUrl != null">call_back_url,</if>
+            <if test="fsSceneType != null">fs_scene_type,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="id != null">#{id},</if>
+            <if test="llmAccountId != null">#{llmAccountId},</if>
+            <if test="callee != null and callee != ''">#{callee},</if>
+            <if test="inboundAlias != null and inboundAlias != ''">#{inboundAlias},</if>
+            <if test="voiceCode != null and voiceCode != ''">#{voiceCode},</if>
+            <if test="voiceSource != null and voiceSource != ''">#{voiceSource},</if>
+            <if test="serviceType != null and serviceType != ''">#{serviceType},</if>
+            <if test="asrProvider != null and asrProvider != ''">#{asrProvider},</if>
+            <if test="aiTransferType != null and aiTransferType != ''">#{aiTransferType},</if>
+            <if test="aiTransferData != null and aiTransferData != ''">#{aiTransferData},</if>
+            <if test="ivrId != null">#{ivrId},</if>
+            <if test="satisfSurveyIvrId != null">#{satisfSurveyIvrId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="callBackUrl != null">#{callBackUrl},</if>
+            <if test="fsSceneType != null">#{fsSceneType},</if>
+        </trim>
+    </insert>
+
+    <update id="updateInboundLlm" parameterType="com.fs.company.vo.easycall.EasyCallInboundLlmVO">
+        update cc_inbound_llm_account
+        <set>
+            <if test="llmAccountId != null">llm_account_id = #{llmAccountId},</if>
+            <if test="callee != null and callee != ''">callee = #{callee},</if>
+            <if test="inboundAlias != null and inboundAlias != ''">inbound_alias = #{inboundAlias},</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code = #{voiceCode},</if>
+            <if test="voiceSource != null and voiceSource != ''">voice_source = #{voiceSource},</if>
+            <if test="serviceType != null and serviceType != ''">service_type = #{serviceType},</if>
+            <if test="asrProvider != null and asrProvider != ''">asr_provider = #{asrProvider},</if>
+            <if test="aiTransferType != null and aiTransferType != ''">ai_transfer_type = #{aiTransferType},</if>
+            <if test="aiTransferData != null and aiTransferData != ''">ai_transfer_data = #{aiTransferData},</if>
+            <if test="ivrId != null">ivr_id = #{ivrId},</if>
+            <if test="satisfSurveyIvrId != null">satisf_survey_ivr_id = #{satisfSurveyIvrId},</if>
+            <if test="callBackUrl != null">call_back_url = #{callBackUrl},</if>
+            <if test="fsSceneType != null">fs_scene_type = #{fsSceneType},</if>
+        </set>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteInboundLlmById" parameterType="Integer">
+        delete from cc_inbound_llm_account where id = #{id}
+    </delete>
+
+    <delete id="deleteInboundLlmByIds" parameterType="String">
+        delete from cc_inbound_llm_account where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectLlmAccountList" resultMap="LlmAccountResult">
+        select id, name, account_json, account_entity, provider_class_name
+        from cc_llm_agent_account
+        <where>
+            <if test="modelsId != null and modelsId.size() > 0"> and id in
+                <foreach item="modelId" collection="modelsId" open="(" separator="," close=")">
+                    #{modelId}
+                </foreach>
+            </if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectLlmAccountById" parameterType="Integer" resultMap="LlmAccountResult">
+        select id, name, account_json, account_entity, provider_class_name
+        from cc_llm_agent_account
+        where id = #{id}
+    </select>
+
+    <select id="selectVoiceCodeByCode" parameterType="String" resultMap="VoiceCodeResult">
+        select voice_code, voice_name, voice_source
+        from cc_tts_aliyun
+        where voice_code = #{voiceCode}
+    </select>
+
+    <!-- 查询ASR提供商列表 -->
+    <select id="selectAsrProviderList" resultType="java.util.Map">
+        select distinct provider as 'key', provider as 'value'
+        from cc_tts_aliyun
+        where provider is not null and provider != ''
+        order by provider
+    </select>
+
+    <!-- 查询TTS音色来源列表 -->
+    <select id="selectVoiceSourceList" resultType="java.util.Map">
+        select distinct voice_source as 'key', voice_source as 'value'
+        from cc_tts_aliyun
+        where voice_source is not null and voice_source != ''
+        order by voice_source
+    </select>
+
+    <!-- 根据音色来源查询音色列表 -->
+    <select id="selectVoiceListBySource" parameterType="String" resultMap="VoiceCodeResult">
+        select voice_code, voice_name, voice_source
+        from cc_tts_aliyun
+        where voice_source = #{voiceSource}
+        and voice_enabled = 1
+        order by priority, id
+    </select>
+
+    <!-- 查询业务组列表 -->
+    <select id="selectBizGroupList" resultMap="BizGroupResult">
+        select group_id, biz_group_name
+        from cc_biz_group
+        order by sort_no, group_id
+    </select>
+
+    <!-- 查询出局网关列表 -->
+    <select id="selectOutboundGatewayList" resultMap="GatewayResult">
+        select id, gw_name, profile_name, caller, callee_prefix, gw_addr, codec, gw_desc, purpose
+        from cc_gateways
+        where purpose in (2, 3)
+        order by id
+    </select>
+
+    <!-- 查询IVR列表 -->
+    <select id="selectIvrList" resultMap="IvrResult">
+        select id, node_name
+        from cc_ivr
+        order by id
+    </select>
+
+    <resultMap type="com.fs.company.domain.EasyCallInboundCdrVO" id="EasyCallInboundCdrResult">
+        <result property="id"    column="id"    />
+        <result property="caller"    column="caller"    />
+        <result property="callee"    column="callee"    />
+        <result property="inboundTime"    column="inbound_time"    />
+        <result property="groupId"    column="group_id"    />
+        <result property="answeredTime"    column="answered_time"    />
+        <result property="extnum"    column="extnum"    />
+        <result property="opnum"    column="opnum"    />
+        <result property="hangupTime"    column="hangup_time"    />
+        <result property="answeredTimeLen"    column="answered_time_len"    />
+        <result property="timeLen"    column="time_len"    />
+        <result property="uuid"    column="uuid"    />
+        <result property="wavFile"    column="wav_file"    />
+        <result property="chatContent"    column="chat_content"    />
+        <result property="asrSeconds"    column="asr_seconds"    />
+        <result property="ttsTimes"    column="tts_times"    />
+        <result property="ttsFlowTokens"    column="tts_flow_tokens"    />
+        <result property="inputTokens"    column="input_tokens"    />
+        <result property="outputTokens"    column="output_tokens"    />
+        <result property="totalCost"    column="total_cost"    />
+        <result property="billingStatus"    column="billing_status"    />
+        <result property="ivrDtmfDigits"    column="ivr_dtmf_digits"    />
+        <result property="hangupCause"    column="hangup_cause"    />
+        <result property="manualAnsweredTime"    column="manual_answered_time"    />
+        <result property="manualAnsweredTimeLen"    column="manual_answered_time_len"    />
+        <result property="groupName"    column="group_name"    />
+    </resultMap>
+
+    <select id="selectInboundCdrList" parameterType="com.fs.company.domain.EasyCallInboundCdrVO" resultMap="EasyCallInboundCdrResult">
+        select c.id, c.caller, c.callee, c.inbound_time, c.group_id, c.answered_time, c.extnum, c.opnum,
+               c.hangup_time, c.answered_time_len, c.time_len, c.uuid, c.wav_file, c.chat_content,
+               c.asr_seconds, c.tts_times, c.tts_flow_tokens, c.input_tokens, c.output_tokens,
+               c.total_cost, c.billing_status, c.ivr_dtmf_digits, c.hangup_cause,
+               c.manual_answered_time, c.manual_answered_time_len,
+               g.biz_group_name as group_name
+        from cc_inbound_cdr c
+        LEFT JOIN cc_biz_group g ON c.group_id = g.group_id
+        <where>
+            c.hangup_time &gt; 0
+            <if test="uuid != null and uuid != ''"> and c.uuid = #{uuid}</if>
+            <if test="caller != null and caller != ''"> and c.caller = #{caller}</if>
+            <if test="callee != null and callee != ''"> and c.callee = #{callee}</if>
+            <if test="extnum != null and extnum != ''"> and c.extnum = #{extnum}</if>
+            <if test="opnum != null and opnum != ''"> and c.opnum = #{opnum}</if>
+            <if test="groupId != null and groupId != ''"> and c.group_id = #{groupId}</if>
+            <if test="billingStatus != null"> and c.billing_status = #{billingStatus}</if>
+            <!-- 呼入时间范围 -->
+            <if test="params != null and params.inboundTimeStart != null">
+                and c.inbound_time &gt;= #{params.inboundTimeStart}
+            </if>
+            <if test="params != null and params.inboundTimeEnd != null">
+                and c.inbound_time &lt;= #{params.inboundTimeEnd}
+            </if>
+            <!-- 接听时间范围 -->
+            <if test="params != null and params.answeredTimeStart != null">
+                and c.answered_time &gt;= #{params.answeredTimeStart}
+            </if>
+            <if test="params != null and params.answeredTimeEnd != null">
+                and c.answered_time &lt;= #{params.answeredTimeEnd}
+            </if>
+            <!-- 挂机时间范围 -->
+            <if test="params != null and params.hangupTimeStart != null">
+                and c.hangup_time &gt;= #{params.hangupTimeStart}
+            </if>
+            <if test="params != null and params.hangupTimeEnd != null">
+                and c.hangup_time &lt;= #{params.hangupTimeEnd}
+            </if>
+            <!-- 通话时长范围 -->
+            <if test="params != null and params.timeLenStart != null">
+                and c.time_len &gt;= #{params.timeLenStart}
+            </if>
+            <if test="params != null and params.timeLenEnd != null">
+                and c.time_len &lt;= #{params.timeLenEnd}
+            </if>
+            <!-- 多租户隔离:可见被叫号码过滤 -->
+            <if test="visibleCallees != null and visibleCallees.size() > 0">
+                and c.callee in
+                <foreach item="calleeItem" collection="visibleCallees" open="(" separator="," close=")">
+                    #{calleeItem}
+                </foreach>
+            </if>
+        </where>
+        order by c.hangup_time desc
+    </select>
+
+</mapper>

+ 12 - 0
fs-service/src/main/resources/mapper/company/EasyCallMapper.xml

@@ -11,5 +11,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         select * from cc_outbound_cdr where uuid = #{uuid}
     </select>
 
+    <select id="selectInboundCallbackInfoByUuid" resultType="com.fs.company.vo.InboundCallInfo">
+        select
+            t1.uuid,
+            t2.fs_company_id,
+            t2.fs_scene_type,
+            t1.chat_content,
+            t1.caller
+        from cc_inbound_cdr t1
+                 inner join cc_inbound_llm_account t2 on t1.callee = t2.callee
+        where  t1.uuid = #{uuid}
+            limit 1
+    </select>
 
 </mapper>

+ 49 - 70
fs-wx-api/src/main/java/com/fs/app/controller/CommonController.java

@@ -1,57 +1,25 @@
 package com.fs.app.controller;
 
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.fs.app.annotation.Login;
-import com.fs.app.param.SendSopParam;
-import com.fs.app.utils.JwtUtils;
-import com.fs.app.websocket.bean.MsgBean;
+import com.fs.app.enums.CmdType;
+import com.fs.company.param.AddWxActionParam;
+import com.fs.app.websocket.bean.ResultMsgVo;
+import com.fs.common.core.domain.ResponseResult;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.mapper.WxContactMapper;
+import com.fs.wxcid.vo.wxvo.AddWxVo;
+import com.fs.wxcid.vo.wxvo.WxSendMsgVo;
 import com.fs.app.websocket.service.WebSocketServer;
 import com.fs.common.core.domain.R;
-import com.fs.common.core.redis.RedisCache;
-import com.fs.company.domain.CompanyConfig;
-import com.fs.company.domain.CompanyMoneyLogs;
-import com.fs.company.service.ICompanyConfigService;
-import com.fs.company.service.ICompanyWxChatService;
-import com.fs.his.service.IFsAppVersionService;
-import com.fs.his.service.IFsCityService;
-import com.fs.his.service.IFsInquiryOrderMsgService;
-import com.fs.his.utils.ConfigUtil;
-import com.fs.sop.domain.QwSopLogs;
-import com.fs.sop.mapper.QwSopLogsMapper;
-import com.fs.system.service.ISysConfigService;
-import com.fs.system.service.ISysDictDataService;
-import com.fs.wx.kf.dto.WeixinKfMsgDTO;
-import com.fs.wx.kf.dto.WeixinKfMsgSendDTO;
-import com.fs.wx.kf.dto.WeixinKfTextMsgDTO;
-import com.fs.wx.kf.service.IWeixinKfService;
-import com.fs.wx.kf.vo.WeixinKfMsgItemVO;
-import com.fs.wx.kf.vo.WeixinKfMsgVO;
-import com.fs.wxUser.service.ICompanyWxUserService;
-import com.google.code.kaptcha.Producer;
-import com.qq.weixin.mp.aes.AesException;
-import com.qq.weixin.mp.aes.WXBizMsgCrypt;
 import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.*;
-import org.w3c.dom.Element;
 
-import javax.annotation.Resource;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.UUID;
-import java.util.logging.Logger;
-
-import static com.fs.common.constant.FsConstants.REDIS_CHAT_NEXTCURSOR;
+import javax.websocket.Session;
 
 
 @Api("公共接口")
@@ -62,42 +30,53 @@ import static com.fs.common.constant.FsConstants.REDIS_CHAT_NEXTCURSOR;
 public class CommonController {
 
     private final WebSocketServer webSocketServer;
-    private final ICompanyWxUserService companyWxUserService;
-    private final ICompanyWxChatService companyWxChatService;
-    private final QwSopLogsMapper qwSopLogsMapper;
-    private final RedisCache redisCache;
-    private final JwtUtils jwtUtils;
-//    private final WebSocketServer webSocketServer;
+    private final WxContactMapper wxContactMapper;
+    private final CompanyWxAccountMapper companyWxAccountMapper;
 
-    @GetMapping("/testSend")
-    public R testSend( ) throws Exception
-    {
-        String json="{\"cmd\":\"sendMsg\",\"msg\":\"test\",\"nickName\":\"123\"}";
-//        String json="{\"cmd\":\"listenMsg\",\"users\":[\"123\"]}";
-        webSocketServer.sendInfo("2020", json);
+    @GetMapping("/syncWx")
+    public R syncWx(String wxId){
+        Session session = WebSocketServer.sessionPools.get(wxId);
+        webSocketServer.sendMessage(session, ResultMsgVo.ok(CmdType.SYNC_CONTACT_PERSON));
         return R.ok();
     }
 
-    @GetMapping("/testListen")
-    public R testListen( ) throws Exception
-    {
-//        String json="{\"cmd\":\"sendMsg\",\"msg\":\"test\",\"nickName\":\"123\"}";
-        String json="{\"cmd\":\"listenMsg\",\"users\":[\"123\"]}";
-        webSocketServer.sendInfo("2020", json);
+    @GetMapping("/testSendMsg")
+    public R syncWx(String wxId, String msg, String remark){
+        Session session = WebSocketServer.sessionPools.get(wxId);
+        webSocketServer.sendMessage(session, ResultMsgVo.<WxSendMsgVo>builder().cmd(CmdType.SEND_MSG).data(new WxSendMsgVo("", remark, msg)).build());
         return R.ok();
     }
-    @GetMapping("/testClickHouse")
-    public R testClickHouse( ) throws Exception{
-        QwSopLogs qwSopLogs = qwSopLogsMapper.selectById("2c42bded-085b-452c-8166-7ed2b070a016");
-        qwSopLogs.setSopId("2324dcea-0a4c-4688-b43d-2d9691ede6b4");
-        qwSopLogsMapper.updateById(qwSopLogs);
+
+    @PostMapping("/sendMsg")
+    public ResponseResult<Void> sendMsg(@RequestBody WxSendMsgVo vo){
+        Session session = WebSocketServer.sessionPools.get(vo.getWxId());
+        webSocketServer.sendMessage(session, ResultMsgVo.<WxSendMsgVo>builder().cmd(CmdType.SEND_MSG).data(vo).build());
+        return ResponseResult.ok();
+    }
+
+    @GetMapping("/addWx")
+    public R addWx(String wxId, String phone, String remark){
+        Session session = WebSocketServer.sessionPools.get(wxId);
+        CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
+        WxContact wxContact = new WxContact();
+        wxContact.setRemark(remark);
+        wxContact.setNickName("测试1");
+        wxContact.setPhone(phone);
+        wxContact.setAccountId(companyWxAccount.getId());
+        wxContact.setCompanyId(companyWxAccount.getCompanyId());
+        wxContact.setCompanyUserId(companyWxAccount.getCompanyUserId());
+        wxContact.setFriends(0);
+        wxContactMapper.insert(wxContact);
+        webSocketServer.sendMessage(session, ResultMsgVo.<AddWxVo>builder().cmd(CmdType.ADD_WX).data(new AddWxVo("", phone, remark,"")).build());
         return R.ok();
     }
 
-    @ApiOperation("获取发送信息")
-    @GetMapping("/getMsgList")
-    public R getMsg(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time){
-        return R.ok().put("data", companyWxUserService.getMsg(1, time));
+    @PostMapping("/addWxAction")
+    public R addWxAction(@RequestBody AddWxActionParam param){
+        String wxId = param.getWxId();
+        Session session = WebSocketServer.sessionPools.get(wxId);
+        webSocketServer.sendMessage(session, ResultMsgVo.<AddWxVo>builder().cmd(CmdType.ADD_WX).data(new AddWxVo(param.getRemark(), param.getPhone(), param.getApplyMsg(), param.getBizJson())).build());
+        return R.ok();
     }
 
 }

+ 28 - 0
fs-wx-api/src/main/java/com/fs/app/enums/CmdType.java

@@ -0,0 +1,28 @@
+package com.fs.app.enums;
+
+public enum CmdType {
+    // 心跳
+    HEARTBEAT,
+    // 初始化备注
+    INIT_REMARK,
+    // 数据返回
+    RETURN,
+    // 退出登录
+    LOGIN_OUT,
+    // 同步用户信息
+    SYNC_INFO,
+    // 同步联系人
+    SYNC_CONTACT_PERSON,
+    // 同步群
+    SYNC_GROUP_CHAT,
+    // 发送消息
+    SEND_MSG,
+    // 消息回调接收
+    SEND_RESULT,
+    // 添加微信
+    ADD_WX,
+    // 添加微信回传
+    ADD_WX_RESULT,
+
+
+}

+ 35 - 0
fs-wx-api/src/main/java/com/fs/app/websocket/bean/ResultMsgVo.java

@@ -0,0 +1,35 @@
+package com.fs.app.websocket.bean;
+
+import com.fs.app.enums.CmdType;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ResultMsgVo<T> {
+
+    private String msg = "ok";
+    private CmdType cmd = CmdType.RETURN;
+    private int code = 200;
+    private int type = 0;
+    private T data;
+
+    public static ResultMsgVo<Void> ok(){
+        return new ResultMsgVo<>();
+    }
+    public static <T> ResultMsgVo<T> ok(T data){
+        return ResultMsgVo.<T>builder().data(data).build();
+    }
+    public static <T> ResultMsgVo<T> ok(CmdType cmd){
+        return ResultMsgVo.<T>builder().cmd(cmd) .build();
+    }
+
+    public static ResultMsgVo<Void> error(String msg){
+        return ResultMsgVo.<Void>builder().msg(msg).code(500).build();
+    }
+
+}

+ 6 - 4
fs-wx-api/src/main/java/com/fs/app/websocket/bean/SendMsgVo.java

@@ -1,5 +1,6 @@
 package com.fs.app.websocket.bean;
 
+import com.fs.app.enums.CmdType;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
@@ -11,10 +12,11 @@ import lombok.NoArgsConstructor;
 @NoArgsConstructor
 public class SendMsgVo {
 
-    private Long userId;
-    private String cmd;
-    private String msg;
-    private String nickName;
+    private CmdType cmd;
+    // 0发送微信1客户端接收
+    private int type;
+    private String dataJson;
+
 
 
 }

+ 150 - 102
fs-wx-api/src/main/java/com/fs/app/websocket/service/WebSocketServer.java

@@ -1,38 +1,28 @@
 package com.fs.app.websocket.service;
 
 
-import cn.hutool.core.util.IdUtil;
-import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONObject;
-import com.fs.ai.service.IBaiduAIService;
-import com.fs.ai.vo.BaiduAIMsgResultVO;
-import com.fs.app.param.SopLogsEditParam;
-import com.fs.app.websocket.bean.MsgBean;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.app.enums.CmdType;
+import com.fs.app.websocket.bean.ResultMsgVo;
 import com.fs.app.websocket.bean.SendMsgVo;
-import com.fs.chat.config.WxConfig;
-import com.fs.chat.domain.ChatKeyword;
-import com.fs.chat.domain.ChatMsg;
-import com.fs.chat.domain.ChatRole;
-import com.fs.chat.domain.ChatSession;
-import com.fs.chat.service.IChatMsgService;
-import com.fs.chat.service.IChatRoleService;
-import com.fs.chat.service.IChatSessionService;
+import com.fs.company.domain.CompanyWxClient;
+import com.fs.company.mapper.CompanyWxClientMapper;
+import com.fs.company.service.CompanyWorkflowEngine;
+import com.fs.company.service.impl.CompanyWxServiceImpl;
+import com.fs.wxcid.domain.CidIpadServer;
+import com.fs.wxcid.mapper.CidIpadServerMapper;
+import com.fs.wxcid.vo.wxvo.*;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
-
-import com.fs.company.domain.CompanyUser;
-import com.fs.company.domain.CompanyWxChat;
-import com.fs.company.service.ICompanyUserService;
-import com.fs.company.service.ICompanyWxChatService;
-import com.fs.course.domain.FsCourseSopLogs;
-import com.fs.course.service.IFsCourseSopLogsService;
-import com.fs.his.domain.FsUser;
-import com.fs.his.service.IFsUserService;
-import com.fs.wx.kf.dto.WeixinKfImageMsgDTO;
-import com.fs.wx.kf.dto.WeixinKfMsgSendDTO;
-import com.fs.wx.kf.dto.WeixinKfTextMsgDTO;
-import com.fs.wxUser.domain.CompanyWxUser;
-import org.apache.commons.lang3.StringUtils;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.mapper.WxContactMapper;
+import com.fs.wxcid.service.IWxMsgLogService;
+import com.hc.openapi.tool.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -41,116 +31,174 @@ import javax.websocket.server.PathParam;
 import javax.websocket.server.ServerEndpoint;
 import java.io.IOException;
 import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.util.Date;
+import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 
-import static com.fs.common.constant.FsConstants.REDIS_CHAT_SESSION;
 
-@ServerEndpoint("/app/webSocket/{uid}")
+@Slf4j
 @Component
+@ServerEndpoint("/app/webSocket/{wxId}")
 public class WebSocketServer {
 
-    private final ICompanyWxChatService companyWxChatService = SpringUtils.getBean(ICompanyWxChatService.class);
-    private final ICompanyUserService companyUserService = SpringUtils.getBean(ICompanyUserService.class);
-
     //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
-    protected static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
-    RedisCache redisCache=SpringUtils.getBean(RedisCache.class);
-    IFsCourseSopLogsService iFsCourseSopLogsService=SpringUtils.getBean(IFsCourseSopLogsService.class);
+    public static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
+    RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
+    CompanyWxAccountMapper accountMapper = SpringUtils.getBean(CompanyWxAccountMapper.class);
+    WxContactMapper wxContactMapper = SpringUtils.getBean(WxContactMapper.class);
+    IWxMsgLogService wxMsgLogService = SpringUtils.getBean(IWxMsgLogService.class);
+    CompanyWxClientMapper companyWxClientMapper = SpringUtils.getBean(CompanyWxClientMapper.class);
+    CompanyWxServiceImpl companyWxService = SpringUtils.getBean(CompanyWxServiceImpl.class);
+    CidIpadServerMapper cidIpadServerMapper = SpringUtils.getBean(CidIpadServerMapper.class);
+    CompanyWorkflowEngine companyWorkflowEngine = SpringUtils.getBean(CompanyWorkflowEngine.class);
 
     //发送消息
-    public void sendMessage(Session session, String message) throws IOException {
-        if(session != null){
+    public <T> void sendMessage(Session session, ResultMsgVo<T> data) {
+        if (session != null) {
             synchronized (session) {
-                System.out.println("发送数据:" + message);
-                session.getBasicRemote().sendText(message);
-            }
-        }
-    }
-    //给指定用户发送信息
-    public void sendInfo(String id, String message){
-        Session session = sessionPools.get(id);
-        try {
-            if(session != null){
-                sendMessage(session, message);
+                log.info("发送数据:{}", data);
+                try {
+                    session.getBasicRemote().sendText(JSON.toJSONString(data));
+                } catch (IOException e) {
+                    log.error("发送消息失败!:{}", data, e);
+                }
             }
-
-        }catch (Exception e){
-            e.printStackTrace();
         }
     }
-
     //建立连接成功调用
     @OnOpen
-    public void onOpen(Session session, @PathParam(value = "uid") String uid){
-
-        sessionPools.put(uid, session);
-
-        System.out.println(uid + "加入webSocket!当前人数为" + sessionPools.size());
-
+    public void onOpen(Session session, @PathParam(value = "wxId") String wxId) {
+        CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
+        if(companyWxAccount == null){
+            sendMessage(session, ResultMsgVo.error("未找到对应微信数据"));
+            return;
+        }
+        sessionPools.put(wxId, session);
+        companyWxAccount.setLoginStatus(1);
+        companyWxAccount.setLoginTime(LocalDateTime.now());
+        accountMapper.updateById(companyWxAccount);
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("remark", companyWxAccount.getRemark());
+        sendMessage(session, ResultMsgVo.<JSONObject>builder().cmd(CmdType.INIT_REMARK).data(jsonObject).build());
+        log.info("{}加入webSocket!当前人数为{}", wxId, sessionPools.size());
     }
 
     //关闭连接时调用
     @OnClose
-    public void onClose(@PathParam(value = "uid") String uid){
-
-        sessionPools.remove(uid);
-        System.out.println(uid + "断开webSocket连接!当前人数为" + sessionPools.size());
+    public void onClose(@PathParam(value = "wxId") String wxId) {
+        sessionPools.remove(wxId);
+        CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
+        if(companyWxAccount != null){
+            companyWxAccount.setLoginStatus(0);
+            companyWxAccount.setOutTime(LocalDateTime.now());
+            companyWxAccount.setOutRemark("连接断开");
+            accountMapper.updateById(companyWxAccount);
+        }
+        log.info("{}断开webSocket连接!当前人数为{}", wxId, sessionPools.size());
     }
 
     //收到客户端信息
     @OnMessage
-    public void onMessage(String message) throws IOException{
+    public void onMessage(String message, @PathParam(value = "wxId") String wxId) {
         SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
-        Session session;
-        System.out.println( "收到数据" + msg.getCmd());
+        if(msg.getType() == 0){
+            return;
+        }
+        Session session = sessionPools.get(wxId);
+        if(session == null){
+            log.error("参数异常:{}", wxId);
+            return;
+        }
+        log.info("收到数据:{}", msg.getCmd());
+        CompanyWxAccount companyWxAccount = accountMapper.selectOne(new QueryWrapper<CompanyWxAccount>().eq("wx_no", wxId));
+        if(companyWxAccount == null){
+            log.error("未找到对应账号:{}", wxId);
+            return;
+        }
         try {
-            switch (msg.getCmd()){
-                case "heartbeat":
-                    session=sessionPools.get(msg.getUserId());
-                    sendMessage(session, JSONObject.toJSONString(msg));
+            switch (msg.getCmd()) {
+                case HEARTBEAT:
+                    log.info("接收心跳:{}", wxId);
                     break;
-                case "sendSop":
-
+                case SYNC_CONTACT_PERSON:
+                    ContactInfoVo contactInfoVo = JSON.parseObject(msg.getDataJson(), ContactInfoVo.class);
+                    if(contactInfoVo == null || StringUtils.isEmpty(contactInfoVo.getRemark())){
+                        log.error("{}同步数据失败,数据缺失:{}", wxId, contactInfoVo);
+                        return;
+                    }
+                    WxContact contact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("remark", contactInfoVo.getRemark()));
+                    if(contact != null){
+                        contact.setNickName(contactInfoVo.getNickName());
+                        contact.setCity(contactInfoVo.getAddress());
+                        contact.setUserName(contactInfoVo.getWxNo());
+                        contact.setUpdateTime(new Date());
+                        wxContactMapper.updateById(contact);
+                    }else{
+                        WxContact contact1 = new WxContact();
+                        contact1.setUserName(contactInfoVo.getWxNo());
+                        contact1.setNickName(contactInfoVo.getNickName());
+                        contact1.setCity(contactInfoVo.getAddress());
+                        contact1.setAccountId(companyWxAccount.getId());
+                        contact1.setCompanyId(companyWxAccount.getCompanyId());
+                        contact1.setCompanyUserId(companyWxAccount.getCompanyUserId());
+                        contact1.setRemark(contactInfoVo.getRemark());
+                        contact1.setCreateTime(new Date());
+                        contact1.setUpdateTime(new Date());
+                        wxContactMapper.insert(contact1);
+                    }
+                    break;
+                case SEND_MSG:
+                    log.info("发送返回:{}", msg);
+                    wxMsgLogService.insertLog(JSON.parseObject(msg.getDataJson(), WxSendResultMsgVo.class), companyWxAccount, 0);
+                    break;
+                case SEND_RESULT:
+                    log.info("接收消息:{}", msg);
+                    wxMsgLogService.insertLog(JSON.parseObject(msg.getDataJson(), WxSendResultMsgVo.class), companyWxAccount, 0);
+                    break;
+                case SYNC_INFO:
+                    SyncInfoVo syncInfoVo = JSON.parseObject(msg.getDataJson(), SyncInfoVo.class);
+                    companyWxAccount.setHeadImgUrl(syncInfoVo.getImg());
+                    companyWxAccount.setPhone(syncInfoVo.getPhone());
+                    accountMapper.updateById(companyWxAccount);
                     break;
-                case "sendMsg":
-                    CompanyWxUser companyWxUser = companyUserService.selectCompanyUserByNickName(msg.getUserId(), msg.getNickName());
-                    if(companyWxUser != null){
-                        saveWxChat(msg.getMsg(), companyWxUser, 1);
+                case ADD_WX_RESULT:
+                    com.fs.wxcid.vo.wxvo.AddResultWxVo addResultWxVo = JSON.parseObject(msg.getDataJson(), com.fs.wxcid.vo.wxvo.AddResultWxVo.class);
+                    log.info("接收到加好友回调:{}", addResultWxVo);
+                    WxContact wxContact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("remark", addResultWxVo.getRemark()).eq("friends", 0));
+                    log.info("更新联系人:{}", wxContact);
+                    wxContact.setFriends(1);
+                    wxContact.setAlias(addResultWxVo.getWxid());
+                    wxContactMapper.updateById(wxContact);
+                    List<CompanyWxClient> clients = companyWxClientMapper.selectWxV2(companyWxAccount.getId(), wxContact.getPhone());
+                    log.info("更新联系人2:{}", clients);
+                    if(clients != null){
+                        clients.parallelStream().forEach(e -> {
+                            e.setIsAdd(1);
+                            e.setRemark(addResultWxVo.getRemark());
+                            e.setWxName(addResultWxVo.getUserName());
+                            e.setSuccessAddTime(LocalDateTime.now());
+                            companyWxClientMapper.updateById(e);
+                            companyWxService.triggerWorkflowOnAddWxSuccess(e.getId());
+                        });
                     }
+//                    if(null != addResultWxVo && StringUtils.isNotBlank(addResultWxVo.getBizJson())){
+//                        JSONObject jsonObject = JSONObject.parseObject(addResultWxVo.getBizJson());
+//                        jsonObject.put("remark",addResultWxVo.getRemark());
+//                        companyWorkflowEngine.addWxSuccess(jsonObject);
+//                    }
                     break;
 
             }
-        }
-        catch (Exception e){
-            System.out.println( "收到数据" + e.getMessage());
+        } catch (Exception e) {
+            log.error("发生错误;{}", e.getMessage());
         }
 
     }
+
     //错误时调用
     @OnError
-    public void onError(Session session, Throwable throwable){
-        System.out.println("发生错误"+throwable.getMessage());
+    public void onError(Session session, Throwable throwable) {
+        log.error("发生错误;{}", throwable.getMessage());
         throwable.printStackTrace();
     }
-
-
-    public SendMsgVo saveWxChat(String msg, CompanyWxUser user, Integer sendType) {
-        SendMsgVo build = SendMsgVo.builder().cmd("sendMsg").msg(msg).nickName(user.getNickName()).build();
-        CompanyWxChat companyWxChat = new CompanyWxChat();
-        companyWxChat.setWxUserId(user.getUserId());
-        companyWxChat.setCompanyId(user.getCompanyId());
-        companyWxChat.setCompanyUserId(user.getCompanyUserId());
-        companyWxChat.setContent(msg);
-        companyWxChat.setSendType(sendType);
-        companyWxChat.setCreateTime(new Date());
-        companyWxChatService.save(companyWxChat);
-        return build;
-    }
-
-
 }

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

@@ -32,14 +32,14 @@ public class WxTask {
 //    public void addWx() {
 //        taskService.addWx(null);
 //    }
-    @Scheduled(cron = "0 0/1 * * * ?")
-    public void addWx4Workflow() {
-        if (saasTaskEnabled) {
-            tenantTaskRunner.runForResponsibleTenant("addWx4Workflow", () ->   taskService.addWx4Workflow(null));
-        } else {
-            taskService.addWx4Workflow(null);
-        }
-    }
+//    @Scheduled(cron = "0 0/1 * * * ?")
+//    public void addWx4Workflow() {
+//        if (saasTaskEnabled) {
+//            tenantTaskRunner.runForResponsibleTenant("addWx4Workflow", () ->   taskService.addWx4Workflow(null));
+//        } else {
+//            taskService.addWx4Workflow(null);
+//        }
+//    }
     @Scheduled(cron = "0 0 0 * * ?")
     public void initAccountNum() {
         if (saasTaskEnabled) {