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

Merge remote-tracking branch 'origin/master'

yuhongqi 1 неделя назад
Родитель
Сommit
a21cb70352
31 измененных файлов с 1253 добавлено и 9 удалено
  1. 42 0
      fs-admin/src/main/java/com/fs/his/controller/FsCompanyController.java
  2. 1 1
      fs-cid-workflow/src/main/resources/application.yml
  3. 72 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java
  4. 42 0
      fs-company/src/main/java/com/fs/company/controller/common/RecordingProxyController.java
  5. 13 0
      fs-company/src/main/java/com/fs/company/controller/company/EasyCallController.java
  6. 7 2
      fs-framework/src/main/java/com/fs/framework/config/DataSourceConfig.java
  7. 1 1
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java
  8. 21 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java
  9. 116 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java
  10. 18 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/AiSipCallUserNewVO.java
  11. 47 0
      fs-service/src/main/java/com/fs/company/domain/CompanyExtensionBind.java
  12. 20 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java
  13. 28 0
      fs-service/src/main/java/com/fs/company/mapper/CcExtNumMapper.java
  14. 79 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyExtensionBindMapper.java
  15. 28 0
      fs-service/src/main/java/com/fs/company/param/BatchCreateExtensionParam.java
  16. 132 0
      fs-service/src/main/java/com/fs/company/service/ICompanyExtensionBindService.java
  17. 17 0
      fs-service/src/main/java/com/fs/company/service/easycall/EasyCallServiceImpl.java
  18. 8 0
      fs-service/src/main/java/com/fs/company/service/easycall/IEasyCallService.java
  19. 248 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyExtensionBindServiceImpl.java
  20. 9 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  21. 12 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  22. 8 0
      fs-service/src/main/java/com/fs/company/vo/AiCallConfigVO.java
  23. 18 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallCreateTaskParam.java
  24. 34 0
      fs-service/src/main/java/com/fs/company/vo/easycall/ExtensionVO.java
  25. 8 1
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java
  26. 31 2
      fs-service/src/main/resources/db/tenant-initTable.sql
  27. 1 1
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallUserMapper.xml
  28. 43 0
      fs-service/src/main/resources/mapper/company/CcExtNumMapper.xml
  29. 144 0
      fs-service/src/main/resources/mapper/company/CompanyExtensionBindMapper.xml
  30. 4 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml
  31. 1 1
      fs-wx-task/src/main/resources/application.yml

+ 42 - 0
fs-admin/src/main/java/com/fs/his/controller/FsCompanyController.java

@@ -15,11 +15,13 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyDeduct;
 import com.fs.company.domain.CompanyRecharge;
+import com.fs.company.param.BatchCreateExtensionParam;
 import com.fs.company.param.CompanyDeductParam;
 import com.fs.company.param.CompanyDivConfigUpdateParam;
 import com.fs.company.param.CompanyRechargeParam;
 import com.fs.company.service.*;
 import com.fs.company.vo.CompanyVO;
+import com.fs.aiSipCall.vo.CcExtNumVo;
 import com.fs.core.utils.OrderCodeUtils;
 import com.fs.course.config.CourseConfig;
 import com.fs.framework.datasource.DynamicDataSourceContextHolder;
@@ -81,6 +83,8 @@ public class FsCompanyController extends BaseController {
     private TenantDataSourceManager tenantDataSourceManager;
     @Autowired
     private TenantInfoMapper tenantInfoMapper;
+    @Autowired
+    private ICompanyExtensionBindService companyExtensionBindService;
     /**
      * 查询诊所管理列表
      */
@@ -382,4 +386,42 @@ public class FsCompanyController extends BaseController {
         if (value.isEmpty())return true;
         return false;
     }
+
+    /**
+     * batchCreateExtension
+     * @return
+     */
+    @PostMapping("/batchCreateExtension")
+    @Log(title = "一键生成分机", businessType = BusinessType.INSERT)
+    public R batchCreateExtension(@RequestBody BatchCreateExtensionParam param){
+        if (param.getCompanyId() == null) {
+            return R.error("公司ID不能为空");
+        }
+        if (param.getCreateNum() == null || param.getCreateNum() <= 0) {
+            return R.error("生成数量必须大于0");
+        }
+        if (StringUtils.isEmpty(param.getPassword())) {
+            param.setPassword("123456");
+        }
+
+        Long tenantId = SecurityUtils.getTenantId();
+        TenantInfo tenantInfo = new TenantInfo();
+        tenantInfo.setId(tenantId);
+//        String tenantCode = ServletUtils.getRequest().getHeader("tenant-code");
+//        TenantInfo tenantInfo = tenantInfoMapper.getTenByCode(tenantCode);
+//        if (tenantInfo == null) {
+//            return R.error("租户信息不存在");
+//        }
+
+        List<CcExtNumVo> extNums = companyExtensionBindService.createExtensionInEasycall(param, tenantId);
+        if (extNums == null || extNums.isEmpty()) {
+            return R.error("分机创建失败,请重试");
+        }
+
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        tenantDataSourceManager.switchTenant(tenantInfo);
+        companyExtensionBindService.bindExtensionToTenant(extNums, param.getCompanyId());
+
+        return R.ok("成功创建" + extNums.size() + "个分机");
+    }
 }

+ 1 - 1
fs-cid-workflow/src/main/resources/application.yml

@@ -10,7 +10,7 @@ spring:
 #    active: druid-sxjz
 #    active: druid-hdt
 #    active: druid-myhk-test
-cid-group-no: 5
+cid-group-no: 3
 tenant-id: 11
 # 配置了服务标记后,请在tenant_service_config 中配置服务应用租户ids信息
 tenant-service-marker: cidWorkflow00

+ 72 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java

@@ -1,17 +1,25 @@
 package com.fs.company.controller.aiSipCall;
 
 import com.fs.aiSipCall.domain.AiSipCallUser;
+import com.fs.aiSipCall.mapper.AiSipCallUserMapper;
 import com.fs.aiSipCall.service.IAiSipCallUserService;
+import com.fs.aiSipCall.vo.AiSipCallUserNewVO;
 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.SecurityUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyExtensionBind;
+import com.fs.company.mapper.CcExtNumMapper;
+import com.fs.company.service.ICompanyExtensionBindService;
+import com.fs.framework.datasource.TenantDataSourceManager;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import org.apache.ibatis.annotations.Param;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -35,6 +43,17 @@ public class AiSipCallUserController extends BaseController
     @Autowired
     private TokenService tokenService;
 
+    @Autowired
+    private CcExtNumMapper ccExtNumMapper;
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+
+
+    @Autowired
+    private ICompanyExtensionBindService companyExtensionBindService;
+
     /**
      * 查询sip用户信息列表
      */
@@ -90,6 +109,27 @@ public class AiSipCallUserController extends BaseController
         return toAjax(aiSipCallUserService.insertAiSipCallUser(aiSipCallUser));
     }
 
+    /**
+     * 新增sip用户信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:aiSipCallUser:add')")
+    @Log(title = "sip用户信息", businessType = BusinessType.INSERT)
+    @PostMapping("/addNew")
+    public AjaxResult addNew(@RequestBody AiSipCallUser aiSipCallUser)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(aiSipCallUser.getCompanyUserId() == null){
+            aiSipCallUser.setCompanyId(loginUser.getCompany().getCompanyId());
+            aiSipCallUser.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+        aiSipCallUser.setCreateBy(loginUser.getUser().getUserName());
+        int i = aiSipCallUserService.insertAiSipCallUserNew(aiSipCallUser);
+        aiSipCallUser.setCreateTime(new Date());
+        ccExtNumMapper.updateUserCodeByExtNum(aiSipCallUser.getExtNum(), aiSipCallUser.getLoginName());
+//        Long tenantId = SecurityUtils.getTenantId();
+//        tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
+        return toAjax(i);
+    }
     /**
      * 修改sip用户信息
      */
@@ -104,6 +144,31 @@ public class AiSipCallUserController extends BaseController
         return toAjax(aiSipCallUserService.updateAiSipCallUser(aiSipCallUser));
     }
 
+    /**
+     * 修改sip用户信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:aiSipCallUser:edit')")
+    @Log(title = "sip用户信息", businessType = BusinessType.UPDATE)
+    @PostMapping("/editNew")
+    public AjaxResult editNew(@RequestBody AiSipCallUser aiSipCallUser)
+    {
+        Long tenantId = SecurityUtils.getTenantId();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        aiSipCallUser.setCompanyId(loginUser.getCompany().getCompanyId());
+//        aiSipCallUser.setCompanyUserId(loginUser.getUser().getUserId());
+        aiSipCallUser.setUpdateBy(loginUser.getUser().getUserName());
+        aiSipCallUser.setUpdateTime(new Date());
+
+        AiSipCallUserNewVO aiSipCallUserNewVO = aiSipCallUserService.updateAiSipCallUserNew(aiSipCallUser);
+        if(StringUtils.isNotBlank(aiSipCallUserNewVO.getOldExtNum())){
+            ccExtNumMapper.updateUserCodeByExtNum(aiSipCallUserNewVO.getOldExtNum(), tenantId + "_" + loginUser.getCompany().getCompanyId() + "_" + aiSipCallUserNewVO.getOldExtNum());
+        }
+        if(StringUtils.isNotBlank(aiSipCallUserNewVO.getNewExtNum()) && StringUtils.isNotBlank(aiSipCallUserNewVO.getNewUserCode())){
+            ccExtNumMapper.updateUserCodeByExtNum(aiSipCallUserNewVO.getNewExtNum(), aiSipCallUserNewVO.getNewUserCode());
+        }
+        return toAjax(1);
+    }
+
     /**
      * 删除sip用户信息
      */
@@ -124,6 +189,13 @@ public class AiSipCallUserController extends BaseController
         return aiSipCallUserService.getUnBindExtnum();
     }
 
+    @GetMapping("/getUnBindExtnumNew/{sipUserId}")
+    public AjaxResult getUnBindExtnumNew(@PathVariable Long sipUserId)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return aiSipCallUserService.getUnBindExtnumNew(loginUser.getCompany().getCompanyId(),sipUserId);
+    }
+
     /**
      * 查询登录销售的sip用户信息列表
      */

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

@@ -117,6 +117,12 @@ public class RecordingProxyController {
                 response.setContentType(guessContentType(url));
             }
 
+            // 7.1 设置 Content-Disposition,使浏览器下载时保存为正确的文件名
+            String fileName = extractFileName(url);
+            if (fileName != null) {
+                response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
+            }
+
             // 8. 转发关键响应头
             String contentLength = connection.getHeaderField("Content-Length");
             if (contentLength != null) {
@@ -231,4 +237,40 @@ public class RecordingProxyController {
         }
         return "application/octet-stream";
     }
+
+    /**
+     * 从录音文件 URL 中提取文件名
+     * 支持直接路径 /recordings/xxx.wav 和 query 参数 ?filename=xxx.wav
+     *
+     * @param url 录音文件 URL
+     * @return 文件名,无法提取时返回 null
+     */
+    private String extractFileName(String url) {
+        if (url == null || url.isEmpty()) {
+            return null;
+        }
+        try {
+            // 先尝试从 query 参数 filename 获取
+            URL parsedUrl = new URL(url);
+            String query = parsedUrl.getQuery();
+            if (query != null && query.contains("filename=")) {
+                for (String param : query.split("&")) {
+                    if (param.startsWith("filename=")) {
+                        return param.substring("filename=".length());
+                    }
+                }
+            }
+            // 从路径中提取文件名
+            String path = parsedUrl.getPath();
+            if (path != null && path.contains("/")) {
+                String name = path.substring(path.lastIndexOf('/') + 1);
+                if (!name.isEmpty()) {
+                    return name;
+                }
+            }
+        } catch (Exception e) {
+            log.debug("提取文件名失败: {}", url);
+        }
+        return null;
+    }
 }

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

@@ -250,4 +250,17 @@ public class EasyCallController extends BaseController {
         String fullUrl = easyCallService.getRecordFileUrl(wavFileUrl, companyId);
         return R.ok().put("data", fullUrl);
     }
+
+    /**
+     * 获取分机号码list
+     * @return
+     */
+    @ApiOperation("获取分机号码list")
+    @GetMapping("/getExtensionList")
+    public R getExtensionList(){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getUser().getCompanyId();
+        List<ExtensionVO> extensionList = easyCallService.getExtensionList(companyId);
+        return  R.ok().put("data",extensionList);
+    }
 }

+ 7 - 2
fs-framework/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -34,13 +34,18 @@ public class DataSourceConfig {
     public DataSource clickhouseDataSource() {
         return new DruidDataSource();
     }
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.easycall.druid.master")
+    public DataSource easyCallSource() {
+        return new DruidDataSource();
+    }
 
     @Bean
     @Primary
-    public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource,@Qualifier("masterDataSource") DataSource masterDataSource) {
+    public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource,@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("easyCallSource") DataSource easyCallSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
         targetDataSources.put(DataSourceType.MASTER, masterDataSource);
-        
+        targetDataSources.put(DataSourceType.EASYCALL.name(), easyCallSource);
         targetDataSources.put(DataSourceType.CLICKHOUSE.name(), clickhouseDataSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }

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

@@ -90,7 +90,7 @@ public class AiSipCallUser extends BaseEntity{
 
     /** 绑定的分机号 */
     @Excel(name = "绑定的分机号")
-    private Long extNum;
+    private String extNum;
     /** 网关字符串 */
     @Excel(name = "网关字符串")
     private String gatewayIds;

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

@@ -2,6 +2,7 @@ package com.fs.aiSipCall.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.aiSipCall.domain.AiSipCallUser;
+import com.fs.aiSipCall.vo.AiSipCallUserNewVO;
 import com.fs.common.core.domain.AjaxResult;
 
 import java.util.List;
@@ -38,6 +39,13 @@ public interface IAiSipCallUserService extends IService<AiSipCallUser>{
      */
     int insertAiSipCallUser(AiSipCallUser aiSipCallUser);
 
+    /**
+     *新增sip用户信息New
+     * @param aiSipCallUser
+     * @return
+     */
+    int insertAiSipCallUserNew(AiSipCallUser aiSipCallUser);
+
     /**
      * 修改sip用户信息
      *
@@ -45,6 +53,12 @@ public interface IAiSipCallUserService extends IService<AiSipCallUser>{
      * @return 结果
      */
     int updateAiSipCallUser(AiSipCallUser aiSipCallUser);
+    /**
+     * 修改sip用户信息New
+     * @param aiSipCallUser
+     * @return
+     */
+    AiSipCallUserNewVO updateAiSipCallUserNew(AiSipCallUser aiSipCallUser);
 
     /**
      * 批量删除sip用户信息
@@ -64,6 +78,13 @@ public interface IAiSipCallUserService extends IService<AiSipCallUser>{
 
     AjaxResult getUnBindExtnum();
 
+    /**
+     * 查询分机号码改
+     * @param companyId
+     * @return
+     */
+    AjaxResult getUnBindExtnumNew(Long companyId,Long sipUserId);
+
     /**
      * 查询aiSIP工具条基础配置参数
      * @param param extNum分机号

+ 116 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java

@@ -6,14 +6,23 @@ import com.fs.aiSipCall.RemoteCommon;
 import com.fs.aiSipCall.domain.AiSipCallUser;
 import com.fs.aiSipCall.mapper.AiSipCallUserMapper;
 import com.fs.aiSipCall.service.IAiSipCallUserService;
+import com.fs.aiSipCall.vo.AiSipCallUserNewVO;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.exception.CustomException;
+import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyBindGatewayMapper;
+import com.fs.company.mapper.CcExtNumMapper;
 import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.company.domain.CompanyExtensionBind;
+import com.fs.company.service.ICompanyExtensionBindService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -33,6 +42,12 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
     @Autowired
     CompanyBindGatewayMapper  companyBindGatewayMapper;
 
+    @Autowired
+    private ICompanyExtensionBindService companyExtensionBindService;
+
+    @Autowired
+    private AiSipCallUserMapper aiSipCallUserMapper;
+
     /**
      * 查询sip用户信息
      * 
@@ -105,6 +120,39 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
         return 0;
     }
 
+    /**
+     * 新增sip用户信息New
+     * @param aiSipCallUser
+     * @return
+     */
+    @Override
+    @Transactional
+    public int insertAiSipCallUserNew(AiSipCallUser aiSipCallUser){
+        CompanyUser companyUser = companyUserMapper.selectCompanyUserByCompanyUserId(aiSipCallUser.getCompanyUserId());
+        if (aiSipCallUser.getCompanyId() == null) {
+            aiSipCallUser.setCompanyId(companyUser.getCompanyId());
+        }
+//        if(aiSipCallUser.getUserId() == null){
+//            aiSipCallUser.setUserId(companyUser.getUserId());
+//        }
+
+        int rows = baseMapper.insertAiSipCallUser(aiSipCallUser);
+        if (rows > 0) {
+            if (aiSipCallUser.getExtNum() != null) {
+//                ccExtNumMapper.updateUserCodeByExtNum(aiSipCallUser.getExtNum(), aiSipCallUser.getLoginName());
+                CompanyExtensionBind bind = companyExtensionBindService.selectUnBindByExtNum(String.valueOf(aiSipCallUser.getExtNum()), aiSipCallUser.getCompanyId());
+                if (bind != null && bind.getCompanyUserId() == null) {
+                    companyExtensionBindService.updateBindByExtId(bind.getExtId(), aiSipCallUser.getCompanyUserId(), aiSipCallUser.getLoginName());
+                }else{
+                    throw new RuntimeException("分机号已被绑定,请刷新后重试");
+                }
+            }
+            if (aiSipCallUser.getCompanyUserId() != null && aiSipCallUser.getUserId() != null) {
+                companyUserMapper.updateCompanyUserByAiSipCall(aiSipCallUser.getCompanyUserId(), aiSipCallUser.getUserId());
+            }
+        }
+        return rows;
+    }
     /**
      * 修改sip用户信息
      * 
@@ -129,6 +177,54 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
         return 0;
     }
 
+    /**
+     * 修改sip用户信息New
+     * @param aiSipCallUser
+     * @return
+     */
+    @Override
+    @Transactional
+    public AiSipCallUserNewVO updateAiSipCallUserNew(AiSipCallUser aiSipCallUser){
+        AiSipCallUserNewVO result= new AiSipCallUserNewVO();
+        CompanyExtensionBind bind = companyExtensionBindService.selectUnBindByExtNum(String.valueOf(aiSipCallUser.getExtNum()), aiSipCallUser.getCompanyId());
+        if (bind != null && bind.getCompanyUserId() == null) {
+            AiSipCallUser oldUser = aiSipCallUserMapper.selectAiSipCallUserByUserId(aiSipCallUser.getUserId());
+            String oldExtNum = (oldUser != null) ? oldUser.getExtNum() : null;
+            String newExtNum = aiSipCallUser.getExtNum();
+            result.setNewExtNum(newExtNum);
+            result.setOldExtNum(oldExtNum);
+            result.setNewUserCode(aiSipCallUser.getLoginName());
+            int rows = aiSipCallUserMapper.updateAiSipCallUser(aiSipCallUser);
+            //解除绑定
+            companyExtensionBindService.clearBindByExtNum(oldExtNum, aiSipCallUser.getCompanyId(), aiSipCallUser.getCompanyUserId());
+            //绑定新分机号
+            companyExtensionBindService.updateBindByExtId(bind.getExtId(), aiSipCallUser.getCompanyUserId(), aiSipCallUser.getLoginName());
+        } else {
+            throw new RuntimeException("分机号已被绑定,请刷新后重试");
+        }
+        return result;
+
+//        AiSipCallUser oldUser = baseMapper.selectAiSipCallUserByUserId(aiSipCallUser.getUserId());
+//        String oldExtNum = (oldUser != null) ? oldUser.getExtNum() : null;
+//        String newExtNum = aiSipCallUser.getExtNum();
+//
+//        int rows = baseMapper.updateAiSipCallUser(aiSipCallUser);
+//        if (rows > 0) {
+//            if (oldExtNum != null && !oldExtNum.equals(newExtNum)) {
+//                ccExtNumMapper.updateUserCodeByExtNum(oldExtNum, null);
+//                companyExtensionBindService.clearBindByExtNum(String.valueOf(oldExtNum), aiSipCallUser.getCompanyId());
+//            }
+//            if (newExtNum != null) {
+//                ccExtNumMapper.updateUserCodeByExtNum(newExtNum, aiSipCallUser.getLoginName());
+//                CompanyExtensionBind bind = companyExtensionBindService.selectUnBindByExtNum(String.valueOf(newExtNum), aiSipCallUser.getCompanyId());
+//                if (bind != null) {
+//                    companyExtensionBindService.updateBindByExtId(bind.getExtId(), aiSipCallUser.getCompanyUserId(), aiSipCallUser.getLoginName());
+//                }
+//            }
+//        }
+//        return rows;
+    }
+
     /**
      * 批量删除sip用户信息
      * 
@@ -170,6 +266,26 @@ public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, A
         return AjaxResult.error();
     }
 
+    /**
+     * 查询分机号码改
+     * @param companyId
+     * @param sipUserId
+     * @return
+     */
+    @Override
+    public AjaxResult getUnBindExtnumNew(Long companyId,Long sipUserId){
+        AiSipCallUser aiSipCallUser = aiSipCallUserMapper.selectAiSipCallUserByUserId(sipUserId);
+        List<CompanyExtensionBind> unBindList = companyExtensionBindService.selectUnBindAndSelfByCompanyId(companyId, aiSipCallUser!=null ? aiSipCallUser.getCompanyUserId() : -1L);
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        for (CompanyExtensionBind bind : unBindList) {
+            Map<String, Object> map = new HashMap<>();
+            map.put("extId", bind.getExtId());
+            map.put("extNum", bind.getExtensionNum());
+            resultList.add(map);
+        }
+        return AjaxResult.success(resultList);
+    }
+
     @Override
     public AjaxResult getToolbarBasicParam(Map<String,String> param) {
         //先使用远程网关

+ 18 - 0
fs-service/src/main/java/com/fs/aiSipCall/vo/AiSipCallUserNewVO.java

@@ -0,0 +1,18 @@
+package com.fs.aiSipCall.vo;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/5/20 09:49
+ * @description
+ */
+@Data
+public class AiSipCallUserNewVO {
+
+    private String oldExtNum;
+
+    private String newExtNum;
+
+    private String newUserCode;
+}

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

@@ -0,0 +1,47 @@
+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_extension_bind
+ *
+ * @author fs
+ * @date 2026-05-19
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyExtensionBind extends BaseEntity{
+
+    /** 主键id */
+    private Long id;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 销售id(为空时没有绑定) */
+    @Excel(name = "销售id", readConverterExp = "为=空时没有绑定")
+    private Long companyUserId;
+
+    /** 分机号码 */
+    @Excel(name = "分机号码")
+    private String extensionNum;
+
+    /** 分机密码 */
+    @Excel(name = "分机密码")
+    private String extensionPass;
+
+    /** easycall使用字段-流水编号 */
+    @Excel(name = "easycall使用字段-流水编号")
+    private Long extId;
+
+    /** easycall使用字段-所属员工/绑定关系 */
+    @Excel(name = "easycall使用字段-所属员工/绑定关系")
+    private String userCode;
+
+
+}

+ 20 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java

@@ -113,6 +113,26 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
      * 违规次数
      * **/
     private Integer violationNum;
+    /**
+     * 转人工接听:1:是,0或无:否
+     */
+    private Integer manualAnswered;
+    /**
+     * 是否被处理:0:未被处理,1:已处理
+     */
+    private Integer handleFlag;
+    /**
+     * 接听分机号码
+     */
+    private String answeredExtNum;
+    /**
+     * 人工接听时间
+     */
+    private Long manualAnsweredTime;
+    /**
+     * 人工接听时长
+     */
+    private Long manualAnsweredTimeLen;
 
     @TableField(exist = false)
     private String companyName;

+ 28 - 0
fs-service/src/main/java/com/fs/company/mapper/CcExtNumMapper.java

@@ -0,0 +1,28 @@
+package com.fs.company.mapper;
+
+import com.fs.aiSipCall.vo.CcExtNumVo;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface CcExtNumMapper {
+
+    @DataSource(DataSourceType.EASYCALL)
+    CcExtNumVo selectLastExtNum();
+
+    @DataSource(DataSourceType.EASYCALL)
+    List<CcExtNumVo> selectExtNumByExtNums(@Param("extNums") List<Long> extNums);
+
+    @DataSource(DataSourceType.EASYCALL)
+    int insertCcExtNum(CcExtNumVo ccExtNumVo);
+
+    @DataSource(DataSourceType.EASYCALL)
+    int batchInsertCcExtNum(@Param("list") List<CcExtNumVo> list);
+
+    @DataSource(DataSourceType.EASYCALL)
+    int updateUserCodeByExtNum(@Param("extNum") String extNum, @Param("userCode") String userCode);
+}

+ 79 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyExtensionBindMapper.java

@@ -0,0 +1,79 @@
+package com.fs.company.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyExtensionBind;
+import com.fs.company.vo.easycall.ExtensionVO;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 公司分机绑定Mapper接口
+ * 
+ * @author fs
+ * @date 2026-05-19
+ */
+public interface CompanyExtensionBindMapper extends BaseMapper<CompanyExtensionBind>{
+    /**
+     * 查询公司分机绑定
+     * 
+     * @param id 公司分机绑定主键
+     * @return 公司分机绑定
+     */
+    CompanyExtensionBind selectCompanyExtensionBindById(Long id);
+
+    /**
+     * 查询公司分机绑定列表
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 公司分机绑定集合
+     */
+    List<CompanyExtensionBind> selectCompanyExtensionBindList(CompanyExtensionBind companyExtensionBind);
+
+    /**
+     * 新增公司分机绑定
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 结果
+     */
+    int insertCompanyExtensionBind(CompanyExtensionBind companyExtensionBind);
+
+    /**
+     * 修改公司分机绑定
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 结果
+     */
+    int updateCompanyExtensionBind(CompanyExtensionBind companyExtensionBind);
+
+    /**
+     * 删除公司分机绑定
+     * 
+     * @param id 公司分机绑定主键
+     * @return 结果
+     */
+    int deleteCompanyExtensionBindById(Long id);
+
+    /**
+     * 批量删除公司分机绑定
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteCompanyExtensionBindByIds(Long[] ids);
+
+    int batchInsertCompanyExtensionBind(@Param("list") List<CompanyExtensionBind> list);
+
+    List<CompanyExtensionBind> selectUnBindByCompanyId(@Param("companyId") Long companyId);
+
+    List<CompanyExtensionBind> selectUnBindAndSelfByCompanyId(@Param("companyId") Long companyId, @Param("companyUserId") Long companyUserId);
+
+    int updateBindByExtId(@Param("extId") Long extId, @Param("companyUserId") Long companyUserId, @Param("userCode") String userCode);
+
+    CompanyExtensionBind selectByExtNumAndCompanyId(@Param("extensionNum") String extensionNum, @Param("companyId") Long companyId);
+
+    int clearBindByExtNum(@Param("extensionNum") String extensionNum, @Param("companyId") Long companyId, @Param("companyUserId") Long companyUserId);
+
+    int updateBindByExtNum(@Param("num") String num, @Param("companyId") Long companyId, @Param("companyUserId") Long companyUserId, @Param("userCode") String userCode);
+
+    List<ExtensionVO> getExtensionList(@Param("companyId") Long companyId);
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/param/BatchCreateExtensionParam.java

@@ -0,0 +1,28 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/5/19 15:04
+ * @description
+ */
+@Data
+public class BatchCreateExtensionParam {
+
+    /**
+     * 公司id
+     */
+    private Long companyId;
+
+    /**
+     * 生成数量
+     */
+    private Integer createNum;
+
+    /**
+     * 分机密码
+     */
+    private String password;
+
+}

+ 132 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyExtensionBindService.java

@@ -0,0 +1,132 @@
+package com.fs.company.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyExtensionBind;
+import com.fs.company.param.BatchCreateExtensionParam;
+import com.fs.aiSipCall.vo.CcExtNumVo;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 公司分机绑定Service接口
+ * 
+ * @author fs
+ * @date 2026-05-19
+ */
+public interface ICompanyExtensionBindService extends IService<CompanyExtensionBind>{
+    /**
+     * 查询公司分机绑定
+     * 
+     * @param id 公司分机绑定主键
+     * @return 公司分机绑定
+     */
+    CompanyExtensionBind selectCompanyExtensionBindById(Long id);
+
+    /**
+     * 查询公司分机绑定列表
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 公司分机绑定集合
+     */
+    List<CompanyExtensionBind> selectCompanyExtensionBindList(CompanyExtensionBind companyExtensionBind);
+
+    /**
+     * 新增公司分机绑定
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 结果
+     */
+    int insertCompanyExtensionBind(CompanyExtensionBind companyExtensionBind);
+
+    /**
+     * 修改公司分机绑定
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 结果
+     */
+    int updateCompanyExtensionBind(CompanyExtensionBind companyExtensionBind);
+
+    /**
+     * 批量删除公司分机绑定
+     * 
+     * @param ids 需要删除的公司分机绑定主键集合
+     * @return 结果
+     */
+    int deleteCompanyExtensionBindByIds(Long[] ids);
+
+    /**
+     * 删除公司分机绑定信息
+     * 
+     * @param id 公司分机绑定主键
+     * @return 结果
+     */
+    int deleteCompanyExtensionBindById(Long id);
+
+    /**
+     * 在easycall数据源中生成分机号并插入cc_ext_num表
+     *
+     * @param param 批量创建分机参数
+     * @param tenantId 租户ID
+     * @return 生成的分机号列表
+     */
+    List<CcExtNumVo> createExtensionInEasycall(BatchCreateExtensionParam param, Long tenantId);
+
+    /**
+     * 在租户数据源中将分机数据写入company_extension_bind表
+     *
+     * @param extNums 分机号数据列表
+     * @param companyId 公司ID
+     */
+    void bindExtensionToTenant(List<CcExtNumVo> extNums, Long companyId);
+
+    /**
+     * 查询公司下未绑定的分机列表
+     *
+     * @param companyId 公司ID
+     * @return 未绑定的分机列表
+     */
+    List<CompanyExtensionBind> selectUnBindByCompanyId(Long companyId);
+
+    /**
+     *
+     * @param companyId
+     * @param companyUserId
+     * @return
+     */
+    List<CompanyExtensionBind> selectUnBindAndSelfByCompanyId(Long companyId, Long companyUserId);
+
+    /**
+     * 根据extId更新绑定信息
+     *
+     * @param extId easycall流水编号
+     * @param companyUserId 销售ID
+     * @param userCode 用户编码
+     */
+    void updateBindByExtId(Long extId, Long companyUserId, String userCode);
+
+    /**
+     * 根据分机号和公司ID查询未绑定记录
+     *
+     * @param extensionNum 分机号码
+     * @param companyId 公司ID
+     * @return 分机绑定记录
+     */
+    CompanyExtensionBind selectUnBindByExtNum(String extensionNum, Long companyId);
+
+    /**
+     * 根据分机号清除绑定信息(company_user_id和user_code置空)
+     *
+     * @param extensionNum 分机号码
+     * @param companyId 公司ID
+     */
+    void clearBindByExtNum(String extensionNum, Long companyId,Long companyUserId);
+
+    /**
+     * 修改分机绑定信息
+     * @param num
+     * @param companyId
+     * @param companyUserId
+     * @param userCode
+     */
+    void updateBindByExtNum(String num,Long companyId,Long companyUserId,String userCode);
+}

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

@@ -13,6 +13,7 @@ import com.fs.company.domain.CompanyConfig;
 import com.fs.company.domain.OutboundLineLimitLog;
 import com.fs.company.dto.OutboundLimitResultVO;
 import com.fs.company.mapper.CompanyConfigMapper;
+import com.fs.company.mapper.CompanyExtensionBindMapper;
 import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.mapper.OutboundLineLimitLogMapper;
 import com.fs.company.vo.easycall.*;
@@ -61,6 +62,9 @@ public class EasyCallServiceImpl implements IEasyCallService {
     @Autowired
     private RedisCache redisCache;
 
+    @Autowired
+    private CompanyExtensionBindMapper companyExtensionBindMapper;
+
     /**
      * EasyCallCenter365 服务器基础地址,从配置文件 easycall.base-url 读取
      */
@@ -685,4 +689,17 @@ public class EasyCallServiceImpl implements IEasyCallService {
         pageResult.setRows(rows == null ? new ArrayList<>() : rows.toJavaList(clazz));
         return pageResult;
     }
+
+    /**
+     * 查询公司分机号
+     * @param companyId 公司id
+     * @return
+     */
+    @Override
+    public List<ExtensionVO> getExtensionList(Long companyId){
+
+        List<ExtensionVO> extensionList = companyExtensionBindMapper.getExtensionList(companyId);
+
+        return extensionList;
+    }
 }

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

@@ -109,4 +109,12 @@ public interface IEasyCallService {
      * @return 完整的录音文件URL
      */
     String getRecordFileUrl(String wavFileUrl, Long companyId);
+
+    /**
+     * 获取外呼分机列表
+     *
+     * @param companyId 公司id
+     * @return 外呼机列表
+     */
+    List<ExtensionVO> getExtensionList(Long companyId);
 }

+ 248 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyExtensionBindServiceImpl.java

@@ -0,0 +1,248 @@
+package com.fs.company.service.impl;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.company.mapper.CompanyExtensionBindMapper;
+import com.fs.company.mapper.CcExtNumMapper;
+import com.fs.company.domain.CompanyExtensionBind;
+import com.fs.company.param.BatchCreateExtensionParam;
+import com.fs.company.service.ICompanyExtensionBindService;
+import com.fs.aiSipCall.vo.CcExtNumVo;
+import com.fs.common.utils.StringUtils;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * 公司分机绑定Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-05-19
+ */
+@Service
+public class CompanyExtensionBindServiceImpl extends ServiceImpl<CompanyExtensionBindMapper, CompanyExtensionBind> implements ICompanyExtensionBindService {
+
+    private static final Logger log = LoggerFactory.getLogger(CompanyExtensionBindServiceImpl.class);
+
+    @Autowired
+    private CcExtNumMapper ccExtNumMapper;
+
+    /**
+     * 查询公司分机绑定
+     * 
+     * @param id 公司分机绑定主键
+     * @return 公司分机绑定
+     */
+    @Override
+    public CompanyExtensionBind selectCompanyExtensionBindById(Long id)
+    {
+        return baseMapper.selectCompanyExtensionBindById(id);
+    }
+
+    /**
+     * 查询公司分机绑定列表
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 公司分机绑定
+     */
+    @Override
+    public List<CompanyExtensionBind> selectCompanyExtensionBindList(CompanyExtensionBind companyExtensionBind)
+    {
+        return baseMapper.selectCompanyExtensionBindList(companyExtensionBind);
+    }
+
+    /**
+     * 新增公司分机绑定
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyExtensionBind(CompanyExtensionBind companyExtensionBind)
+    {
+        companyExtensionBind.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCompanyExtensionBind(companyExtensionBind);
+    }
+
+    /**
+     * 修改公司分机绑定
+     * 
+     * @param companyExtensionBind 公司分机绑定
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyExtensionBind(CompanyExtensionBind companyExtensionBind)
+    {
+        return baseMapper.updateCompanyExtensionBind(companyExtensionBind);
+    }
+
+    /**
+     * 批量删除公司分机绑定
+     * 
+     * @param ids 需要删除的公司分机绑定主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyExtensionBindByIds(Long[] ids)
+    {
+        return baseMapper.deleteCompanyExtensionBindByIds(ids);
+    }
+
+    /**
+     * 删除公司分机绑定信息
+     * 
+     * @param id 公司分机绑定主键
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyExtensionBindById(Long id)
+    {
+        return baseMapper.deleteCompanyExtensionBindById(id);
+    }
+
+    @Override
+    public List<CcExtNumVo> createExtensionInEasycall(BatchCreateExtensionParam param, Long tenantId) {
+        int createNum = param.getCreateNum();
+        String password = param.getPassword();
+        Long companyId = param.getCompanyId();
+
+        CcExtNumVo lastExtNum = ccExtNumMapper.selectLastExtNum();
+        long currentMaxExtNum = (lastExtNum != null && lastExtNum.getExtNum() != null) ? lastExtNum.getExtNum() : 0;
+
+        List<CcExtNumVo> allNewExtNums = new ArrayList<>();
+        long nextExtNum = currentMaxExtNum + 1;
+        int userCodeSeq = 1;
+
+        for (int i = 0; i < createNum; i++) {
+            CcExtNumVo vo = new CcExtNumVo();
+            vo.setExtNum(nextExtNum + i);
+            vo.setExtPass(password);
+            vo.setUserCode(tenantId + "_" + companyId + "_" + userCodeSeq);
+            userCodeSeq++;
+            allNewExtNums.add(vo);
+        }
+
+        int maxRetry = 10;
+        for (int retry = 0; retry < maxRetry; retry++) {
+            List<Long> extNumsToCheck = allNewExtNums.stream()
+                    .map(CcExtNumVo::getExtNum)
+                    .collect(Collectors.toList());
+
+            List<CcExtNumVo> existingExtNums = ccExtNumMapper.selectExtNumByExtNums(extNumsToCheck);
+            if (CollectionUtils.isEmpty(existingExtNums)) {
+                break;
+            }
+
+            List<Long> existingExtNumValues = existingExtNums.stream()
+                    .map(CcExtNumVo::getExtNum)
+                    .collect(Collectors.toList());
+
+            long maxExisting = existingExtNumValues.stream().max(Long::compareTo).orElse(currentMaxExtNum);
+            nextExtNum = maxExisting + 1;
+
+            List<CcExtNumVo> replaceList = new ArrayList<>();
+            for (CcExtNumVo vo : allNewExtNums) {
+                if (existingExtNumValues.contains(vo.getExtNum())) {
+                    vo.setExtNum(nextExtNum);
+                    nextExtNum++;
+                    replaceList.add(vo);
+                }
+            }
+
+            if (CollectionUtils.isEmpty(replaceList)) {
+                break;
+            }
+        }
+
+        try {
+            ccExtNumMapper.batchInsertCcExtNum(allNewExtNums);
+        } catch (Exception e) {
+            log.warn("批量插入cc_ext_num失败,尝试逐条插入补偿", e);
+            List<CcExtNumVo> successList = new ArrayList<>();
+            for (CcExtNumVo vo : allNewExtNums) {
+                try {
+                    ccExtNumMapper.insertCcExtNum(vo);
+                    successList.add(vo);
+                } catch (Exception ex) {
+                    log.warn("插入分机号{}失败,可能已被其他线程占用,尝试补偿", vo.getExtNum());
+                    int compensateRetry = 0;
+                    boolean compensated = false;
+                    while (compensateRetry < 5 && !compensated) {
+                        CcExtNumVo currentLast = ccExtNumMapper.selectLastExtNum();
+                        long newExtNum = (currentLast != null && currentLast.getExtNum() != null) ? currentLast.getExtNum() + 1 : 1;
+                        vo.setExtNum(newExtNum);
+                        try {
+                            ccExtNumMapper.insertCcExtNum(vo);
+                            compensated = true;
+                        } catch (Exception ex2) {
+                            compensateRetry++;
+                        }
+                    }
+                    if (compensated) {
+                        successList.add(vo);
+                    }
+                }
+            }
+            allNewExtNums = successList;
+        }
+
+        return allNewExtNums;
+    }
+
+    @Override
+    public void bindExtensionToTenant(List<CcExtNumVo> extNums, Long companyId) {
+        List<CompanyExtensionBind> bindList = new ArrayList<>();
+        for (CcExtNumVo extNumVo : extNums) {
+            CompanyExtensionBind bind = new CompanyExtensionBind();
+            bind.setCompanyId(companyId);
+            bind.setExtensionNum(String.valueOf(extNumVo.getExtNum()));
+            bind.setExtensionPass(extNumVo.getExtPass());
+            bind.setExtId(extNumVo.getExtId());
+            bind.setUserCode(extNumVo.getUserCode());
+            bind.setCreateTime(DateUtils.getNowDate());
+            bindList.add(bind);
+        }
+
+        int batchSize = 100;
+        for (int i = 0; i < bindList.size(); i += batchSize) {
+            int end = Math.min(i + batchSize, bindList.size());
+            List<CompanyExtensionBind> batch = bindList.subList(i, end);
+            baseMapper.batchInsertCompanyExtensionBind(batch);
+        }
+    }
+
+    @Override
+    public List<CompanyExtensionBind> selectUnBindByCompanyId(Long companyId) {
+        return baseMapper.selectUnBindByCompanyId(companyId);
+    }
+
+    @Override
+    public List<CompanyExtensionBind> selectUnBindAndSelfByCompanyId(Long companyId, Long companyUserId){
+        return baseMapper.selectUnBindAndSelfByCompanyId(companyId,companyUserId);
+    }
+
+    @Override
+    public void updateBindByExtId(Long extId, Long companyUserId, String userCode) {
+        baseMapper.updateBindByExtId(extId, companyUserId, userCode);
+    }
+
+    @Override
+    public CompanyExtensionBind selectUnBindByExtNum(String extensionNum, Long companyId) {
+        return baseMapper.selectByExtNumAndCompanyId(extensionNum, companyId);
+    }
+
+    @Override
+    public void clearBindByExtNum(String extensionNum, Long companyId,Long companyUserId) {
+        baseMapper.clearBindByExtNum(extensionNum, companyId,companyUserId);
+    }
+
+    @Override
+    public void updateBindByExtNum(String num, Long companyId, Long companyUserId, String userCode) {
+        baseMapper.updateBindByExtNum(num, companyId,companyUserId, userCode);
+    }
+}

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

@@ -400,6 +400,15 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                         companyVoiceRoboticCallLog.setViolationNum(resultDTO.getViolationCount());
                     }
 
+                    //AI外呼 转人工逻辑
+                    if(StringUtils.isNotBlank(result.getAcdOpnum()) && !"robot".equalsIgnoreCase(result.getAcdOpnum())){
+                        companyVoiceRoboticCallLog.setManualAnswered(1);
+                        companyVoiceRoboticCallLog.setHandleFlag(0);
+                        companyVoiceRoboticCallLog.setAnsweredExtNum(result.getAcdOpnum());
+                        companyVoiceRoboticCallLog.setManualAnsweredTime(result.getManualAnsweredTime());
+                        companyVoiceRoboticCallLog.setManualAnsweredTimeLen(result.getManualAnsweredTimeLen());
+                    }
+
                     baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
                     // 更新用户标签
                     crmCustomerPropertyService.addPropertyByCallLog(companyVoiceRoboticCallLog);

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

@@ -603,6 +603,18 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
                 // 模型参数
                 createParam.setTtsModels(callConfigVo.getTtsModels());
 
+                if (callConfigVo.getExtensionList() != null && !callConfigVo.getExtensionList().isEmpty()) {
+                    createParam.setAiTransferType("extension");
+                    StringBuilder aiTransferExtNumber = new StringBuilder();
+                    callConfigVo.getExtensionList().forEach(extension -> {
+                        aiTransferExtNumber.append(extension.getExtensionNum()).append(" ");
+                    });
+                    if (null != aiTransferExtNumber && aiTransferExtNumber.length() > 0) {
+                        aiTransferExtNumber.deleteCharAt(aiTransferExtNumber.length() - 1);
+                    }
+                    createParam.setAiTransferExtNumber(aiTransferExtNumber.toString());
+                }
+
                 EasyCallTaskVO task = easyCallService.createTask(createParam, null);
                 if (task == null || task.getBatchId() == null) {
                     log.error("createSipTask: 创建 EasyCall 任务失败 - workflowInstanceId: {}", workFlowId);

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

@@ -1,7 +1,10 @@
 package com.fs.company.vo;
 
+import com.fs.company.vo.easycall.ExtensionVO;
 import lombok.Data;
 
+import java.util.List;
+
 /**
  * @author MixLiu
  * @date 2026/2/3 10:13
@@ -69,4 +72,9 @@ public class AiCallConfigVO {
 
     /** 最大并发数 */
     private Integer maxConcurrency;
+
+    /**
+     * 分机选择列表
+     */
+    private List<ExtensionVO> extensionList;
 }

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

@@ -35,4 +35,22 @@ public class EasyCallCreateTaskParam {
     private Double avgCallEndProcessTimeLen;
 
     private String ttsModels;
+
+    /** aiTransferType */
+    private String aiTransferType;
+
+    /** aiTransferData */
+    private String aiTransferData;
+
+    /** aiTransferGroupId */
+    private String aiTransferGroupId;
+
+    /** aiTransferGatewayId */
+    private String aiTransferGatewayId;
+
+    /** aiTransferGatewayDestNumber */
+    private String aiTransferGatewayDestNumber;
+
+    /** aiTransferExtNumber */
+    private String aiTransferExtNumber;
 }

+ 34 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/ExtensionVO.java

@@ -0,0 +1,34 @@
+package com.fs.company.vo.easycall;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/5/20 15:16
+ * @description
+ */
+@Data
+public class ExtensionVO {
+    /**
+     * 分机号码
+     */
+    private String extensionNum;
+
+    /**
+     * 公司id
+     */
+    private Long companyId;
+    /**
+     * 销售id
+     */
+    private Long companyUserId;
+    /**
+     * 销售名称
+     */
+    private String companyUserName;
+    /**
+     * 分机id EasyCall 的分机ID
+     */
+    private Integer extId;
+
+}

+ 8 - 1
fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java

@@ -188,6 +188,13 @@ public class CrmCustomer extends BaseEntity
     @Excel(name = "历史沟通记录")
     private String historicalCommunication;
 
-
+    //    有效客户:1:有效,0或者空:无效
+    private Integer effectiveCustomer;
+    //    AI外呼备注
+    private String aiCallRemark;
+    //    最后一次设置有效时录音
+    private String effectiveRecordPath;
+    //    最后一次设置有效时外呼记录id
+    private Long lastEffectiveCallphoneLogId;
 
 }

+ 31 - 2
fs-service/src/main/resources/db/tenant-initTable.sql

@@ -2729,6 +2729,11 @@ CREATE TABLE `company_voice_robotic_call_log_callphone`
     `call_type`        int NULL DEFAULT NULL COMMENT '外呼类型',
     `is_warning` tinyint NULL DEFAULT 0 COMMENT '是否警告(0否1是)用于敏感词',
     `violation_num` int NULL DEFAULT 0 COMMENT '违规数量',
+    `manual_answered` tinyint NULL DEFAULT NULL COMMENT '人工接听:1:是,0或无:否',
+    `handle_flag` tinyint NULL DEFAULT 0 COMMENT '是否被处理:0:未被处理,1:已处理',
+    `answered_ext_num` varchar(200) NULL DEFAULT NULL COMMENT '接听分机号码',
+    `manual_answered_time` bigint NULL DEFAULT NULL COMMENT '人工接听时间',
+    `manual_answered_time_len` bigint NULL DEFAULT NULL COMMENT '人工接听时长',
     PRIMARY KEY (`log_id`) USING BTREE,
     INDEX              `robotic_id_and_caller_id_idx`(`robotic_id`, `caller_id`) USING BTREE,
     INDEX              `company_and_company_user_idx`(`company_id`, `company_user_id`) USING BTREE,
@@ -3066,6 +3071,10 @@ CREATE TABLE `crm_customer`
     `is_pool_rule`          tinyint NULL DEFAULT 1 COMMENT '是否采用入公海规则 0:否 1:是',
     `sys_visit_time`        datetime NULL DEFAULT NULL COMMENT '系统自建跟进时间',
     `historical_communication` text NULL COMMENT '历史沟通内容',
+    `effective_customer` tinyint NULL DEFAULT NULL COMMENT '有效客户:1:有效,0或者空:无效',
+    `ai_call_remark` varchar(500) NULL DEFAULT NULL COMMENT 'AI外呼备注',
+    `effective_record_path` varchar(1000) NULL DEFAULT NULL COMMENT '最后一次设置有效时录音',
+    `last_effective_callphone_log_id` bigint NULL DEFAULT NULL COMMENT '最后一次设置有效时外呼记录id',
     PRIMARY KEY (`customer_id`) USING BTREE,
     UNIQUE INDEX `customer_code`(`customer_code`) USING BTREE,
     INDEX                   `create_user_id`(`create_user_id`) USING BTREE,
@@ -18377,7 +18386,7 @@ CREATE TABLE `company_inbound_bind`
 DROP TABLE IF EXISTS `ai_sip_call_user`;
 CREATE TABLE `ai_sip_call_user`
 (
-    `user_id`         bigint                                                       NOT NULL COMMENT '用户ID',
+    `user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
     `dept_id`         bigint NULL DEFAULT NULL COMMENT '部门ID',
     `login_name`      varchar(30) NOT NULL COMMENT '登录账号',
     `user_name`       varchar(30) NULL DEFAULT '' COMMENT '用户昵称',
@@ -18404,7 +18413,7 @@ CREATE TABLE `ai_sip_call_user`
     `company_user_id` bigint NULL DEFAULT NULL COMMENT '销售ID',
     `gateway_ids`     varchar(255)  NULL DEFAULT NULL COMMENT '网关ids',
     PRIMARY KEY (`user_id`) USING BTREE
-) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'sip用户信息表' ROW_FORMAT = DYNAMIC;
+) ENGINE = InnoDB  AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'sip用户信息表' ROW_FORMAT = DYNAMIC;
 
 
 -- ----------------------------
@@ -18575,5 +18584,25 @@ CREATE TABLE `crm_customer_property`
     PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
 
+-- ----------------------------
+-- Table structure for company_extension_bind
+-- ----------------------------
+DROP TABLE IF EXISTS `company_extension_bind`;
+CREATE TABLE `company_extension_bind`
+(
+    `id`              bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
+    `company_id`      bigint NOT NULL COMMENT '公司id',
+    `company_user_id` bigint NULL DEFAULT NULL COMMENT '销售id(为空时没有绑定)',
+    `extension_num`   varchar(50) NULL DEFAULT NULL COMMENT '分机号码',
+    `extension_pass`  varchar(50) NULL DEFAULT NULL COMMENT '分机密码',
+    `ext_id`          int NULL DEFAULT NULL COMMENT 'easycall使用字段-流水编号',
+    `user_code`       varchar(32) NULL DEFAULT NULL COMMENT 'easycall使用字段-所属员工/绑定关系',
+    `create_time`     datetime NULL DEFAULT NULL,
+    `create_by`       varchar(255) NULL DEFAULT NULL,
+    PRIMARY KEY (`id`) USING BTREE,
+    INDEX             `company_company_user_idx`(`company_id`, `company_user_id`) USING BTREE,
+    INDEX             `company_extension_idx`(`company_id`, `extension_num`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
 SET
 FOREIGN_KEY_CHECKS = 1;

+ 1 - 1
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallUserMapper.xml

@@ -66,7 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where user_id = #{userId}
     </select>
         
-    <insert id="insertAiSipCallUser" parameterType="AiSipCallUser">
+    <insert id="insertAiSipCallUser" parameterType="AiSipCallUser" useGeneratedKeys="true" keyProperty="userId">
         insert into ai_sip_call_user
         <trim prefix="(" suffix=")" suffixOverrides=",">
             <if test="userId != null">user_id,</if>

+ 43 - 0
fs-service/src/main/resources/mapper/company/CcExtNumMapper.xml

@@ -0,0 +1,43 @@
+<?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.CcExtNumMapper">
+
+    <resultMap type="com.fs.aiSipCall.vo.CcExtNumVo" id="CcExtNumResult">
+        <result property="extId"    column="ext_id"    />
+        <result property="extNum"    column="ext_num"    />
+        <result property="extPass"    column="ext_pass"    />
+        <result property="userCode"    column="user_code"    />
+    </resultMap>
+
+    <select id="selectLastExtNum" resultMap="CcExtNumResult">
+        select ext_id, ext_num, ext_pass, user_code from cc_ext_num order by ext_id desc limit 1
+    </select>
+
+    <select id="selectExtNumByExtNums" resultMap="CcExtNumResult">
+        select ext_id, ext_num, ext_pass, user_code from cc_ext_num
+        where ext_num in
+        <foreach item="extNum" collection="extNums" open="(" separator="," close=")">
+            #{extNum}
+        </foreach>
+    </select>
+
+    <insert id="insertCcExtNum" parameterType="com.fs.aiSipCall.vo.CcExtNumVo" useGeneratedKeys="true" keyProperty="extId">
+        insert into cc_ext_num (ext_num, ext_pass, user_code)
+        values (#{extNum}, #{extPass}, #{userCode})
+    </insert>
+
+    <insert id="batchInsertCcExtNum" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="extId">
+        insert into cc_ext_num (ext_num, ext_pass, user_code)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.extNum}, #{item.extPass}, #{item.userCode})
+        </foreach>
+    </insert>
+
+    <update id="updateUserCodeByExtNum">
+        update cc_ext_num set user_code = #{userCode} where ext_num = #{extNum}
+    </update>
+
+</mapper>

+ 144 - 0
fs-service/src/main/resources/mapper/company/CompanyExtensionBindMapper.xml

@@ -0,0 +1,144 @@
+<?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.CompanyExtensionBindMapper">
+    
+    <resultMap type="CompanyExtensionBind" id="CompanyExtensionBindResult">
+        <result property="id"    column="id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="extensionNum"    column="extension_num"    />
+        <result property="extensionPass"    column="extension_pass"    />
+        <result property="extId"    column="ext_id"    />
+        <result property="userCode"    column="user_code"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+    </resultMap>
+
+    <sql id="selectCompanyExtensionBindVo">
+        select id, company_id, company_user_id, extension_num, extension_pass, ext_id, user_code, create_time, create_by from company_extension_bind
+    </sql>
+
+    <select id="selectCompanyExtensionBindList" parameterType="CompanyExtensionBind" resultMap="CompanyExtensionBindResult">
+        <include refid="selectCompanyExtensionBindVo"/>
+        <where>  
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="extensionNum != null  and extensionNum != ''"> and extension_num = #{extensionNum}</if>
+            <if test="extensionPass != null  and extensionPass != ''"> and extension_pass = #{extensionPass}</if>
+            <if test="extId != null "> and ext_id = #{extId}</if>
+            <if test="userCode != null  and userCode != ''"> and user_code = #{userCode}</if>
+        </where>
+    </select>
+    
+    <select id="selectCompanyExtensionBindById" parameterType="Long" resultMap="CompanyExtensionBindResult">
+        <include refid="selectCompanyExtensionBindVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertCompanyExtensionBind" parameterType="CompanyExtensionBind" useGeneratedKeys="true" keyProperty="id">
+        insert into company_extension_bind
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="extensionNum != null">extension_num,</if>
+            <if test="extensionPass != null">extension_pass,</if>
+            <if test="extId != null">ext_id,</if>
+            <if test="userCode != null">user_code,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="extensionNum != null">#{extensionNum},</if>
+            <if test="extensionPass != null">#{extensionPass},</if>
+            <if test="extId != null">#{extId},</if>
+            <if test="userCode != null">#{userCode},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+         </trim>
+    </insert>
+
+    <update id="updateCompanyExtensionBind" parameterType="CompanyExtensionBind">
+        update company_extension_bind
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="extensionNum != null">extension_num = #{extensionNum},</if>
+            <if test="extensionPass != null">extension_pass = #{extensionPass},</if>
+            <if test="extId != null">ext_id = #{extId},</if>
+            <if test="userCode != null">user_code = #{userCode},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCompanyExtensionBindById" parameterType="Long">
+        delete from company_extension_bind where id = #{id}
+    </delete>
+
+    <delete id="deleteCompanyExtensionBindByIds" parameterType="String">
+        delete from company_extension_bind where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <insert id="batchInsertCompanyExtensionBind" parameterType="java.util.List">
+        insert into company_extension_bind (company_id, company_user_id, extension_num, extension_pass, ext_id, user_code, create_time)
+        values
+        <foreach item="item" collection="list" separator=",">
+            (#{item.companyId}, #{item.companyUserId}, #{item.extensionNum}, #{item.extensionPass}, #{item.extId}, #{item.userCode}, #{item.createTime})
+        </foreach>
+    </insert>
+
+    <select id="selectUnBindByCompanyId" resultMap="CompanyExtensionBindResult">
+        <include refid="selectCompanyExtensionBindVo"/>
+        where company_id = #{companyId} and (company_user_id is null or company_user_id = 0)
+    </select>
+
+    <select id="selectUnBindAndSelfByCompanyId" resultMap="CompanyExtensionBindResult">
+        <include refid="selectCompanyExtensionBindVo"/>
+        where company_id = #{companyId} and (company_user_id is null or company_user_id =#{companyUserId})
+    </select>
+
+    <update id="updateBindByExtId">
+        update company_extension_bind
+        set company_user_id = #{companyUserId}, user_code = #{userCode}
+        where ext_id = #{extId}
+    </update>
+
+    <select id="selectByExtNumAndCompanyId" resultMap="CompanyExtensionBindResult">
+        <include refid="selectCompanyExtensionBindVo"/>
+        where extension_num = #{extensionNum} and company_id = #{companyId}
+    </select>
+
+    <update id="clearBindByExtNum">
+        update company_extension_bind
+        set company_user_id = null, user_code = null
+        where extension_num = #{extensionNum} and company_id = #{companyId} and company_user_id = #{companyUserId}
+    </update>
+
+    <update id="updateBindByExtNum">
+        update company_extension_bind
+        set company_user_id = #{companyUserId}, user_code = #{userCode}
+        where extension_num = #{num} and company_id = #{companyId} and (company_user_id is null or company_user_id = #{companyUserId})
+    </update>
+
+    <select id="getExtensionList" resultType="com.fs.company.vo.easycall.ExtensionVO">
+        SELECT
+            t1.company_id,
+            t1.company_user_id,
+            t1.extension_num,
+            t1.ext_id,
+            t2.nick_name as companyUserName
+        FROM
+            company_extension_bind  t1
+                left join company_user t2 on t1.company_user_id = t2.user_id
+        WHERE
+            t1.company_id = #{companyId}
+    </select>
+</mapper>

+ 4 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

@@ -268,6 +268,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="qwName != null">qw_name = #{qwName},</if>
             <if test="wxContactId != null">wx_contact_id = #{wxContactId},</if>
             <if test="historicalCommunication != null">historical_communication = #{historicalCommunication},</if>
+            <if test="effectiveCustomer != null">effective_customer = #{effectiveCustomer},</if>
+            <if test="aiCallRemark != null">ai_call_remark = #{aiCallRemark},</if>
+            <if test="effectiveRecordPath != null">effective_record_path = #{effectiveRecordPath},</if>
+            <if test="lastEffectiveCallphoneLogId != null">last_effective_callphone_log_id = #{lastEffectiveCallphoneLogId},</if>
         </trim>
         where customer_id = #{customerId}
     </update>

+ 1 - 1
fs-wx-task/src/main/resources/application.yml

@@ -14,6 +14,6 @@ spring:
 #    active: druid-sxjz
 #    active: druid-hdt
 #    active: druid-myhk-test
-cid-group-no: 5
+cid-group-no: 3
 # 配置了服务标记后,请在tenant_service_config 中配置服务应用租户ids信息
 tenant-service-marker: wxTask00