فهرست منبع

cid呼入改动

lmx 1 روز پیش
والد
کامیت
bfc22671eb
25فایلهای تغییر یافته به همراه1231 افزوده شده و 15 حذف شده
  1. 31 0
      fs-common/src/main/java/com/fs/common/annotation/CallbackIpCheck.java
  2. 109 0
      fs-company/src/main/java/com/fs/company/aspectj/CallbackIpCheckAspect.java
  3. 104 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyInboundCallManageController.java
  4. 16 5
      fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java
  5. 31 0
      fs-service/src/main/java/com/fs/company/domain/CompanyInboundBind.java
  6. 111 0
      fs-service/src/main/java/com/fs/company/domain/EasyCallInboundCdrVO.java
  7. 66 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyInboundBindMapper.java
  8. 11 1
      fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java
  9. 4 0
      fs-service/src/main/java/com/fs/company/mapper/EasyCallMapper.java
  10. 1 1
      fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java
  11. 18 0
      fs-service/src/main/java/com/fs/company/param/InboundCallbackParam.java
  12. 61 0
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundBindService.java
  13. 10 1
      fs-service/src/main/java/com/fs/company/service/ICompanyInboundCallManageService.java
  14. 3 0
      fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java
  15. 93 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundBindServiceImpl.java
  16. 55 3
      fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java
  17. 28 0
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  18. 202 0
      fs-service/src/main/java/com/fs/company/util/IpCheckUtil.java
  19. 4 0
      fs-service/src/main/java/com/fs/company/vo/CidConfigVO.java
  20. 43 0
      fs-service/src/main/java/com/fs/company/vo/InboundCallInfo.java
  21. 22 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallInboundLlmVO.java
  22. 7 0
      fs-service/src/main/java/com/fs/his/config/CidPhoneConfig.java
  23. 77 0
      fs-service/src/main/resources/mapper/company/CompanyInboundBindMapper.xml
  24. 111 2
      fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml
  25. 13 0
      fs-service/src/main/resources/mapper/company/EasyCallMapper.xml

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

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

@@ -0,0 +1,109 @@
+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.company.util.IpCheckUtil;
+import com.fs.common.utils.IpUtil;
+import com.fs.company.domain.CompanyConfig;
+import com.fs.company.mapper.CompanyConfigMapper;
+import com.fs.company.vo.CidConfigVO;
+import com.fs.his.config.CidPhoneConfig;
+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();
+    }
+
+
+}

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

@@ -1,11 +1,17 @@
 package com.fs.company.controller.company;
 
+import com.fs.aicall.domain.CcLlmAgentAccount;
+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.EasyCallBizGroupVO;
@@ -14,12 +20,17 @@ import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
 import com.fs.company.vo.easycall.EasyCallIvrVO;
 import com.fs.company.vo.easycall.EasyCallLlmAccountVO;
 import com.fs.company.vo.easycall.EasyCallVoiceCodeVO;
+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
@@ -36,11 +47,28 @@ public class CompanyInboundCallManageController extends BaseController {
     @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);
@@ -59,7 +87,19 @@ public class CompanyInboundCallManageController extends BaseController {
      */
     @GetMapping("/llmAccountList")
     public AjaxResult getLlmAccountList() {
-        List<EasyCallLlmAccountVO> list = inboundLlmMapper.selectLlmAccountList();
+        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);
     }
 
@@ -74,15 +114,19 @@ public class CompanyInboundCallManageController extends BaseController {
     /**
      * 新增呼入大模型配置
      */
+    @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) {
@@ -94,8 +138,11 @@ public class CompanyInboundCallManageController extends BaseController {
      */
     @Log(title = "呼入大模型配置", businessType = BusinessType.DELETE)
     @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPermi('inboundCallManage:delete')")
     public AjaxResult remove(@PathVariable String ids) {
-        return toAjax(inboundCallManageService.deleteInboundLlmByIds(ids));
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        return toAjax(inboundCallManageService.deleteInboundLlmByIds(ids,companyId));
     }
 
     /**
@@ -197,10 +244,65 @@ public class CompanyInboundCallManageController extends BaseController {
         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());

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

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

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

@@ -0,0 +1,31 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+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;
+
+
+}

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

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

@@ -0,0 +1,66 @@
+package com.fs.company.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyInboundBind;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 呼入线路模型绑定关系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);
+}

+ 11 - 1
fs-service/src/main/java/com/fs/company/mapper/EasyCallInboundLlmMapper.java

@@ -2,6 +2,7 @@ package com.fs.company.mapper;
 
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.EasyCallInboundCdrVO;
 import com.fs.company.vo.easycall.EasyCallBizGroupVO;
 import com.fs.company.vo.easycall.EasyCallGatewayVO;
 import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
@@ -91,7 +92,7 @@ public interface EasyCallInboundLlmMapper {
      * @return 大模型账户列表
      */
     @DataSource(DataSourceType.EASYCALL)
-    List<EasyCallLlmAccountVO> selectLlmAccountList();
+    List<EasyCallLlmAccountVO> selectLlmAccountList(@Param("modelsId") List<Long> modelsId);
 
     /**
      * 根据ID查询大模型账户
@@ -159,4 +160,13 @@ public interface EasyCallInboundLlmMapper {
      */
     @DataSource(DataSourceType.EASYCALL)
     List<EasyCallIvrVO> selectIvrList();
+
+    /**
+     * 查询呼入通话记录列表
+     *
+     * @param vo 查询条件
+     * @return 通话记录列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<EasyCallInboundCdrVO> selectInboundCdrList(EasyCallInboundCdrVO vo);
 }

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

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

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

@@ -162,7 +162,7 @@ public class EntryCustomerParam {
     private Integer sceneType;
     //对话图
     private String dialogue;
-    //投流id
+    //投流id ||在呼入场景下 此id存储值为呼入cdr的uuid
     private String traceId;
 
     private String remark;

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

@@ -0,0 +1,18 @@
+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;
+}

+ 61 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyInboundBindService.java

@@ -0,0 +1,61 @@
+package com.fs.company.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyInboundBind;
+
+/**
+ * 呼入线路模型绑定关系Service接口
+ * 
+ * @author fs
+ * @date 2026-04-27
+ */
+public interface ICompanyInboundBindService extends IService<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 ids 需要删除的呼入线路模型绑定关系主键集合
+     * @return 结果
+     */
+    int deleteCompanyInboundBindByIds(Long[] ids);
+
+    /**
+     * 删除呼入线路模型绑定关系信息
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 结果
+     */
+    int deleteCompanyInboundBindById(Long id);
+}

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

@@ -1,5 +1,6 @@
 package com.fs.company.service;
 
+import com.fs.company.domain.EasyCallInboundCdrVO;
 import com.fs.company.vo.easycall.EasyCallInboundLlmVO;
 
 import java.util.List;
@@ -65,5 +66,13 @@ public interface ICompanyInboundCallManageService {
      * @param ids ID字符串,逗号分隔
      * @return 影响行数
      */
-    int deleteInboundLlmByIds(String ids);
+    int deleteInboundLlmByIds(String ids,Long companyId);
+
+    /**
+     * 查询呼入通话记录列表
+     *
+     * @param vo 查询条件
+     * @return 通话记录列表
+     */
+    List<EasyCallInboundCdrVO> selectInboundCdrList(EasyCallInboundCdrVO vo);
 }

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

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

+ 93 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundBindServiceImpl.java

@@ -0,0 +1,93 @@
+package com.fs.company.service.impl;
+
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.company.mapper.CompanyInboundBindMapper;
+import com.fs.company.domain.CompanyInboundBind;
+import com.fs.company.service.ICompanyInboundBindService;
+
+/**
+ * 呼入线路模型绑定关系Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-04-27
+ */
+@Service
+public class CompanyInboundBindServiceImpl extends ServiceImpl<CompanyInboundBindMapper, CompanyInboundBind> implements ICompanyInboundBindService {
+
+    /**
+     * 查询呼入线路模型绑定关系
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 呼入线路模型绑定关系
+     */
+    @Override
+    public CompanyInboundBind selectCompanyInboundBindById(Long id)
+    {
+        return baseMapper.selectCompanyInboundBindById(id);
+    }
+
+    /**
+     * 查询呼入线路模型绑定关系列表
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 呼入线路模型绑定关系
+     */
+    @Override
+    public List<CompanyInboundBind> selectCompanyInboundBindList(CompanyInboundBind companyInboundBind)
+    {
+        return baseMapper.selectCompanyInboundBindList(companyInboundBind);
+    }
+
+    /**
+     * 新增呼入线路模型绑定关系
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyInboundBind(CompanyInboundBind companyInboundBind)
+    {
+        companyInboundBind.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCompanyInboundBind(companyInboundBind);
+    }
+
+    /**
+     * 修改呼入线路模型绑定关系
+     * 
+     * @param companyInboundBind 呼入线路模型绑定关系
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyInboundBind(CompanyInboundBind companyInboundBind)
+    {
+        return baseMapper.updateCompanyInboundBind(companyInboundBind);
+    }
+
+    /**
+     * 批量删除呼入线路模型绑定关系
+     * 
+     * @param ids 需要删除的呼入线路模型绑定关系主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyInboundBindByIds(Long[] ids)
+    {
+        return baseMapper.deleteCompanyInboundBindByIds(ids);
+    }
+
+    /**
+     * 删除呼入线路模型绑定关系信息
+     * 
+     * @param id 呼入线路模型绑定关系主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyInboundBindById(Long id)
+    {
+        return baseMapper.deleteCompanyInboundBindById(id);
+    }
+}

+ 55 - 3
fs-service/src/main/java/com/fs/company/service/impl/CompanyInboundCallManageServiceImpl.java

@@ -1,13 +1,20 @@
 package com.fs.company.service.impl;
 
 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 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业务层处理
@@ -20,6 +27,9 @@ public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallM
     @Autowired
     private EasyCallInboundLlmMapper inboundLlmMapper;
 
+    @Autowired
+    CompanyInboundBindMapper companyInboundBindMapper;
+
     /**
      * 查询呼入大模型配置
      *
@@ -61,7 +71,19 @@ public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallM
      */
     @Override
     public int insertInboundLlm(EasyCallInboundLlmVO vo) {
-        return inboundLlmMapper.insertInboundLlm(vo);
+        Boolean b = checkCalleeInboundLlm(vo.getCallee());
+        if(b){
+          throw new RuntimeException("被叫号码已存在,不能重复插入");
+        }
+        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;
     }
 
     /**
@@ -72,6 +94,10 @@ public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallM
      */
     @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);
     }
 
@@ -93,7 +119,33 @@ public class CompanyInboundCallManageServiceImpl implements ICompanyInboundCallM
      * @return 影响行数
      */
     @Override
-    public int deleteInboundLlmByIds(String ids) {
-        return inboundLlmMapper.deleteInboundLlmByIds(Convert.toStrArray(ids));
+    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;
     }
 }

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

@@ -15,12 +15,15 @@ import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.CompanyConfig;
 import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.mapper.CompanyVoiceRoboticMapper;
+import com.fs.company.mapper.EasyCallMapper;
 import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.param.InboundCallbackParam;
 import com.fs.company.service.ICompanyConfigService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.IGeneralCustomerEntryService;
 import com.fs.company.util.PhoneNumberUtil;
 import com.fs.company.vo.DictVO;
+import com.fs.company.vo.InboundCallInfo;
 import com.fs.config.ai.AiHostProper;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.domain.CrmCustomerProperty;
@@ -80,6 +83,8 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
     CompanyVoiceRoboticServiceImpl companyVoiceRoboticServiceImpl;
     @Autowired
     CrmCustomerPropertyServiceImpl crmCustomerPropertyService;
+    @Autowired
+    EasyCallMapper easyCallMapper;
     /**
      * 录入客户
      *
@@ -405,4 +410,27 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
         return !now.isBefore(start) || !now.isAfter(end);
     }
 
+    /**
+     * 呼入回调
+     * @param param
+     */
+    @Override
+    public void inboundCallback(InboundCallbackParam param){
+        try {
+            Thread.sleep(5000L);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        if (param == null || StringUtils.isBlank(param.getUuid())){
+            return;
+        }
+        InboundCallInfo info  = easyCallMapper.selectInboundCallbackInfoByUuid(param.getUuid());
+        EntryCustomerParam entry = new EntryCustomerParam();
+        entry.setTraceId(param.getUuid());
+        entry.setCompanyId(info.getFsCompanyId());
+        entry.setSceneType(info.getFsSceneType());
+        entry.setMobile(info.getCaller());
+        entryCustomer(entry);
+    }
+
 }

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

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

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

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

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

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

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

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

@@ -53,4 +53,11 @@ public class CidPhoneConfig implements Serializable {
      * 配置回调地址
      */
     private String callbackUrl;
+
+    /**
+     * 呼入回调地址
+     */
+    private String inboundCallbackUrl;
+
+
 }

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

+ 111 - 2
fs-service/src/main/resources/mapper/company/EasyCallInboundLlmMapper.xml

@@ -56,7 +56,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </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 from cc_inbound_llm_account
+        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">
@@ -71,6 +71,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <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>
@@ -85,7 +93,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where callee = #{callee}
     </select>
 
-    <insert id="insertInboundLlm" parameterType="com.fs.company.vo.easycall.EasyCallInboundLlmVO">
+    <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>
@@ -100,6 +108,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <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>
@@ -114,6 +125,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <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>
 
@@ -131,6 +145,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <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>
@@ -149,6 +165,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <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>
 
@@ -211,4 +234,90 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         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>

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

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