lmx 2 дней назад
Родитель
Сommit
8f6d65ef60
41 измененных файлов с 3331 добавлено и 0 удалено
  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. 30 0
      fs-service/src/main/java/com/fs/company/domain/CompanyInboundBind.java
  5. 42 0
      fs-service/src/main/java/com/fs/company/domain/CompanySiptaskInfo.java
  6. 42 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceCloneRef.java
  7. 111 0
      fs-service/src/main/java/com/fs/company/domain/EasyCallInboundCdrVO.java
  8. 67 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyInboundBindMapper.java
  9. 65 0
      fs-service/src/main/java/com/fs/company/mapper/CompanySiptaskInfoMapper.java
  10. 33 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceCloneRefMapper.java
  11. 167 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java
  12. 21 0
      fs-service/src/main/java/com/fs/company/param/AddWxActionParam.java
  13. 19 0
      fs-service/src/main/java/com/fs/company/param/InboundCallbackParam.java
  14. 78 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  15. 15 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceCloneRefService.java
  16. 174 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java
  17. 37 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceCloneRefServiceImpl.java
  18. 326 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java
  19. 202 0
      fs-service/src/main/java/com/fs/company/util/IpCheckUtil.java
  20. 43 0
      fs-service/src/main/java/com/fs/company/vo/InboundCallInfo.java
  21. 16 0
      fs-service/src/main/java/com/fs/his/dto/SendResultDetailDTO.java
  22. 20 0
      fs-service/src/main/java/com/fs/ipad/vo/WxBaseVo.java
  23. 9 0
      fs-service/src/main/java/com/fs/ipad/vo/WxTxtVo.java
  24. 38 0
      fs-service/src/main/java/com/fs/wxcid/dto/callback/WxCallbackVo.java
  25. 68 0
      fs-service/src/main/java/com/fs/wxcid/service/IWxMsgLogService.java
  26. 131 0
      fs-service/src/main/java/com/fs/wxcid/service/impl/WxMsgLogServiceImpl.java
  27. 15 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/AddResultWxVo.java
  28. 19 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/AddWxVo.java
  29. 18 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/ContactInfoVo.java
  30. 16 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/MsgResultVo.java
  31. 16 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/SyncInfoVo.java
  32. 15 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/WxSendMsgVo.java
  33. 15 0
      fs-service/src/main/java/com/fs/wxcid/vo/wxvo/WxSendResultMsgVo.java
  34. 58 0
      fs-service/src/main/java/com/fs/wxwork/service/WxIpadService.java
  35. 307 0
      fs-service/src/main/java/com/fs/wxwork/utils/WxHttpUtil.java
  36. 77 0
      fs-service/src/main/resources/mapper/company/CompanyInboundBindMapper.xml
  37. 79 0
      fs-service/src/main/resources/mapper/company/CompanySiptaskInfoMapper.xml
  38. 100 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceCloneRefMapper.xml
  39. 323 0
      fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml
  40. 28 0
      fs-wx-api/src/main/java/com/fs/app/enums/CmdType.java
  41. 35 0
      fs-wx-api/src/main/java/com/fs/app/websocket/bean/ResultMsgVo.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());
+            }
+        }
+    }
+}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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