wjj 1 semana atrás
pai
commit
441727e365
100 arquivos alterados com 11611 adições e 26 exclusões
  1. 9 9
      fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java
  2. 48 0
      fs-common/src/main/java/com/fs/common/constant/Constants.java
  3. 36 0
      fs-common/src/main/java/com/fs/common/core/page/TableDataInfo.java
  4. 2 1
      fs-common/src/main/java/com/fs/common/enums/DataSourceType.java
  5. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallBizGroupController.java
  6. 101 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallGatewayController.java
  7. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallLlmAgentAccountController.java
  8. 167 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java
  9. 163 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallPhoneController.java
  10. 257 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallTaskController.java
  11. 152 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java
  12. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallVoiceTtsAliyunController.java
  13. 357 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentAccountController.java
  14. 134 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentProviderController.java
  15. 153 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmKbCatController.java
  16. 121 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowServerController.java
  17. 111 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java
  18. 175 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogAddwxController.java
  19. 162 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java
  20. 142 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java
  21. 112 14
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  22. 181 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java
  23. 20 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  24. 170 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java
  25. 78 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyTemplateController.java
  26. 14 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  27. 9 1
      fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java
  28. 134 0
      fs-service/src/main/java/com/fs/aiSipCall/RemoteCommon.java
  29. 38 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallBizGroup.java
  30. 85 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallGateway.java
  31. 70 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallLlmAgentAccount.java
  32. 97 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallOutboundCdr.java
  33. 270 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallPhone.java
  34. 164 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallTask.java
  35. 99 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java
  36. 50 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallVoiceTtsAliyun.java
  37. 55 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/CcCustCallRecord.java
  38. 86 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/CcCustInfo.java
  39. 16 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/AiCallListModel.java
  40. 24 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CallTaskStatModel.java
  41. 14 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CommonCallListModel.java
  42. 13 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CommonPhoneModel.java
  43. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallBizGroupMapper.java
  44. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallGatewayMapper.java
  45. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallLlmAgentAccountMapper.java
  46. 71 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallOutboundCdrMapper.java
  47. 96 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallPhoneMapper.java
  48. 67 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallTaskMapper.java
  49. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallUserMapper.java
  50. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallVoiceTtsAliyunMapper.java
  51. 21 0
      fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordByUuidQueryParams.java
  52. 36 0
      fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordQueryParams.java
  53. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallBizGroupService.java
  54. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallGatewayService.java
  55. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallLlmAgentAccountService.java
  56. 74 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallOutboundCdrService.java
  57. 84 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallPhoneService.java
  58. 74 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallTaskService.java
  59. 72 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java
  60. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallVoiceTtsAliyunService.java
  61. 110 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallBizGroupServiceImpl.java
  62. 112 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallGatewayServiceImpl.java
  63. 108 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallLlmAgentAccountServiceImpl.java
  64. 543 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java
  65. 429 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallPhoneServiceImpl.java
  66. 278 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallTaskServiceImpl.java
  67. 183 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java
  68. 108 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallVoiceTtsAliyunServiceImpl.java
  69. 85 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/CharsetKit.java
  70. 1010 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/Convert.java
  71. 239 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/DateUtils.java
  72. 90 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/StrFormatter.java
  73. 669 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/StringUtils.java
  74. 34 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/UuidGenerator.java
  75. 59 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/ApiCallRecordQueryVo.java
  76. 45 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/CallPhoneExportVo.java
  77. 30 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/CcExtNumVo.java
  78. 172 0
      fs-service/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java
  79. 23 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/OkHttpConfig.java
  80. 34 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/TtsConfig.java
  81. 27 0
      fs-service/src/main/java/com/fs/aiSoundReplication/config/VoiceCloneConfig.java
  82. 42 0
      fs-service/src/main/java/com/fs/aiSoundReplication/exception/ErrorCodeEnum.java
  83. 21 0
      fs-service/src/main/java/com/fs/aiSoundReplication/exception/VoiceCloneException.java
  84. 50 0
      fs-service/src/main/java/com/fs/aiSoundReplication/mapper/VoiceCloneMapper.java
  85. 20 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/BaseResponse.java
  86. 33 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/StatusResponse.java
  87. 13 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TrainingStatusRequest.java
  88. 83 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsChargeParam.java
  89. 61 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsRequest.java
  90. 54 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsResponse.java
  91. 12 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/UploadResponse.java
  92. 38 0
      fs-service/src/main/java/com/fs/aiSoundReplication/param/VoiceCloneRequest.java
  93. 58 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/TtsService.java
  94. 51 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/VoiceCloneService.java
  95. 548 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/TtsServiceImpl.java
  96. 349 0
      fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/VoiceCloneServiceImpl.java
  97. 45 0
      fs-service/src/main/java/com/fs/aiSoundReplication/util/FileToMultipartConverterUtil.java
  98. 47 0
      fs-service/src/main/java/com/fs/aiSoundReplication/util/FileUtil.java
  99. 3 0
      fs-service/src/main/java/com/fs/aicall/domain/BaseDomain.java
  100. 162 0
      fs-service/src/main/java/com/fs/aicall/domain/CcCallTask.java

+ 9 - 9
fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticController.java

@@ -143,33 +143,33 @@ public class CompanyVoiceRoboticController extends BaseController
      * 删除机器人外呼任务
      */
 	@GetMapping("/getTypes")
-    public R getTypes(){
-        List<GetairobotResult> getairobotlist = aiCallService.getairobotlist();
-        List<EditDialogResult> editDialogResults = aiCallService.queryDialog();
+    public R getTypes(Long companyId){
+        List<GetairobotResult> getairobotlist = aiCallService.getairobotlist(companyId);
+        List<EditDialogResult> editDialogResults = aiCallService.queryDialog(companyId);
         return R.ok().put("robot", getairobotlist).put("dialog", editDialogResults);
     }
     /**
      * 查询状态
      */
     @GetMapping("/statusList")
-    public R statusList(String ids){
+    public R statusList(String ids,Long companyId){
         List<String> list = Arrays.stream(ids.split(",")).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
-        return R.ok().put("data", list.stream().map(e -> aiCallService.queryCallTaskInfo(TaskInfo.builder().taskID(e).build())).collect(Collectors.toMap(QueryCallTaskInfoResult::getTaskID, e -> e)));
+        return R.ok().put("data", list.stream().map(e -> aiCallService.queryCallTaskInfo(TaskInfo.builder().taskID(e).build(),companyId)).collect(Collectors.toMap(QueryCallTaskInfoResult::getTaskID, e -> e)));
     }
     /**
      * 启动任务
      */
 	@GetMapping("/startRobotic")
-    public R startRobotic(String taskId){
-        aiCallService.startCallTask(TaskInfo.builder().taskID(taskId).build());
+    public R startRobotic(String taskId,Long companyId){
+        aiCallService.startCallTask(TaskInfo.builder().taskID(taskId).build(),companyId);
         return R.ok();
     }
     /**
      * 停止任务
      */
 	@GetMapping("/stopRobotic")
-    public R stopRobotic(String taskId){
-        aiCallService.stopCallTask(TaskInfo.builder().taskID(taskId).build());
+    public R stopRobotic(String taskId,Long companyId){
+        aiCallService.stopCallTask(TaskInfo.builder().taskID(taskId).build(),companyId);
         return R.ok();
     }
     /**

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

@@ -172,4 +172,52 @@ public class Constants
     public static final String LOOKUP_LDAP = "ldap://";
 
     public static final Integer PAGE_SIZE =10;
+    /**
+     * CID 执行任务ID
+     */
+    public static final String TASK_ID = "cid:task:";
+    /**
+     * 外呼机器人打电话
+     */
+    public static final String CELL_PHONE = "cellPhone";
+    /**
+     * 添加微信
+     */
+    public static final String ADD_WX = "addWx";
+    /**
+     * 企微添加个微
+     */
+    public static final String QW_ADD_WX = "qwAddWx";
+    /**
+     * 发送短信
+     */
+    public static final String SEND_MSG = "sendMsg";
+    /**
+     * CID 执行下一个 roboticID + calleesId
+     */
+    public static final String CID_NEXT_TASK_ID = "cid:next:task:";
+
+    /**
+     * 工作流加微超时检测 Key: workflow:addwx:timeout:{workflowInstanceId}:{wxClientId}
+     * Value: 超时时间戳
+     */
+    public static final String WORKFLOW_ADD_WX_TIMEOUT = "workflow:addwx:timeout:";
+
+    /**
+     * 工作流加微执行状态标记 Key: workflow:addwx:executed:{workflowInstanceId}:{wxClientId}
+     * Value: 1-已执行
+     * 用于实现回调成功和超时互斥,只执行一个
+     */
+    public static final String WORKFLOW_ADD_WX_EXECUTED = "workflow:addwx:executed:";
+    /**
+     * 工作流企微加个微执行状态标记 Key: workflow:addwx:executed:{workflowInstanceId}:{wxClientId}
+     * Value: 1-已执行
+     * 用于实现回调成功和超时互斥,只执行一个
+     */
+    public static final String WORKFLOW_QW_ADD_WX_EXECUTED = "workflow:qwaddwx:executed:";
+    /**
+     * 工作流企微加个微超时检测 Key: workflow:addwx:timeout:{workflowInstanceId}:{wxClientId}
+     * Value: 超时时间戳
+     */
+    public static final String WORKFLOW_QW_ADD_WX_TIMEOUT = "workflow:qwaddwx:timeout:";
 }

+ 36 - 0
fs-common/src/main/java/com/fs/common/core/page/TableDataInfo.java

@@ -24,6 +24,12 @@ public class TableDataInfo implements Serializable
     /** 消息内容 */
     private String msg;
 
+    private long totalRecordCount;
+
+    private long successRecordCount;
+
+    private long todayRecordCount;
+
     /**
      * 表格数据对象
      */
@@ -82,4 +88,34 @@ public class TableDataInfo implements Serializable
     {
         this.msg = msg;
     }
+
+    public long getTotalRecordCount()
+    {
+        return totalRecordCount;
+    }
+
+    public void setTotalRecordCount(long totalRecordCount)
+    {
+        this.totalRecordCount = totalRecordCount;
+    }
+
+    public long getSuccessRecordCount()
+    {
+        return successRecordCount;
+    }
+
+    public void setSuccessRecordCount(long successRecordCount)
+    {
+        this.successRecordCount = successRecordCount;
+    }
+
+    public long getTodayRecordCount()
+    {
+        return todayRecordCount;
+    }
+
+    public void setTodayRecordCount(long todayRecordCount)
+    {
+        this.todayRecordCount = todayRecordCount;
+    }
 }

+ 2 - 1
fs-common/src/main/java/com/fs/common/enums/DataSourceType.java

@@ -19,5 +19,6 @@ public enum DataSourceType
      * 从库
      */
     SLAVE,
-    SopREAD
+    SopREAD,
+    EASYCALL
 }

+ 97 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallBizGroupController.java

@@ -0,0 +1,97 @@
+package com.fs.company.controller.aiSipCall;
+
+import com.fs.aiSipCall.domain.AiSipCallBizGroup;
+import com.fs.aiSipCall.service.IAiSipCallBizGroupService;
+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.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼技能组Controller
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@RestController
+@RequestMapping("/company/aiSipCall/bizGroup")
+public class AiSipCallBizGroupController extends BaseController
+{
+    @Autowired
+    private IAiSipCallBizGroupService aiSipCallBizGroupService;
+
+    /**
+     * 查询aiSIP外呼技能组列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:bizGroup:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AiSipCallBizGroup aiSipCallBizGroup)
+    {
+        startPage();
+        List<AiSipCallBizGroup> list = aiSipCallBizGroupService.selectAiSipCallBizGroupList(aiSipCallBizGroup);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出aiSIP外呼技能组列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:bizGroup:export')")
+    @Log(title = "aiSIP外呼技能组", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AiSipCallBizGroup aiSipCallBizGroup)
+    {
+        List<AiSipCallBizGroup> list = aiSipCallBizGroupService.selectAiSipCallBizGroupList(aiSipCallBizGroup);
+        ExcelUtil<AiSipCallBizGroup> util = new ExcelUtil<>(AiSipCallBizGroup.class);
+        return util.exportExcel(list, "aiSIP外呼技能组数据");
+    }
+
+    /**
+     * 获取aiSIP外呼技能组详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:bizGroup:query')")
+    @GetMapping(value = "/{groupId}")
+    public AjaxResult getInfo(@PathVariable("groupId") Long groupId)
+    {
+        return AjaxResult.success(aiSipCallBizGroupService.selectAiSipCallBizGroupByGroupId(groupId));
+    }
+
+    /**
+     * 新增aiSIP外呼技能组
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:bizGroup:add')")
+    @Log(title = "aiSIP外呼技能组", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AiSipCallBizGroup aiSipCallBizGroup)
+    {
+        return toAjax(aiSipCallBizGroupService.insertAiSipCallBizGroup(aiSipCallBizGroup));
+    }
+
+    /**
+     * 修改aiSIP外呼技能组
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:bizGroup:edit')")
+    @Log(title = "aiSIP外呼技能组", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody AiSipCallBizGroup aiSipCallBizGroup)
+    {
+        return toAjax(aiSipCallBizGroupService.updateAiSipCallBizGroup(aiSipCallBizGroup));
+    }
+
+    /**
+     * 删除aiSIP外呼技能组
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:bizGroup:remove')")
+    @Log(title = "aiSIP外呼技能组", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{groupIds}")
+    public AjaxResult remove(@PathVariable Long[] groupIds)
+    {
+        return toAjax(aiSipCallBizGroupService.deleteAiSipCallBizGroupByGroupIds(groupIds));
+    }
+}

+ 101 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallGatewayController.java

@@ -0,0 +1,101 @@
+package com.fs.company.controller.aiSipCall;
+
+import com.fs.aiSipCall.domain.AiSipCallGateway;
+import com.fs.aiSipCall.service.IAiSipCallGatewayService;
+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.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * aiSIP外呼网关Controller
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@RestController
+@RequestMapping("/company/aiSipCall/gateway")
+public class AiSipCallGatewayController extends BaseController
+{
+    @Autowired
+    private IAiSipCallGatewayService aiSipCallGatewayService;
+
+    /**
+     * 查询aiSIP外呼网关列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:gateway:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AiSipCallGateway aiSipCallGateway)
+    {
+        startPage();
+        List<AiSipCallGateway> list = aiSipCallGatewayService.selectAiSipCallGatewayList(aiSipCallGateway);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出aiSIP外呼网关列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:gateway:export')")
+    @Log(title = "aiSIP外呼网关", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AiSipCallGateway aiSipCallGateway)
+    {
+        List<AiSipCallGateway> list = aiSipCallGatewayService.selectAiSipCallGatewayList(aiSipCallGateway);
+        ExcelUtil<AiSipCallGateway> util = new ExcelUtil<>(AiSipCallGateway.class);
+        return util.exportExcel(list, "aiSIP外呼网关数据");
+    }
+
+    /**
+     * 获取aiSIP外呼网关详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:gateway:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(aiSipCallGatewayService.selectAiSipCallGatewayById(id));
+    }
+
+    /**
+     * 新增aiSIP外呼网关
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:gateway:add')")
+    @Log(title = "aiSIP外呼网关", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AiSipCallGateway aiSipCallGateway)
+    {
+        if(aiSipCallGateway.getUpdateTime() == null){
+            aiSipCallGateway.setUpdateTime(new Date());
+        }
+        return toAjax(aiSipCallGatewayService.insertAiSipCallGateway(aiSipCallGateway));
+    }
+
+    /**
+     * 修改aiSIP外呼网关
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:gateway:edit')")
+    @Log(title = "aiSIP外呼网关", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody AiSipCallGateway aiSipCallGateway)
+    {
+        return toAjax(aiSipCallGatewayService.updateAiSipCallGateway(aiSipCallGateway));
+    }
+
+    /**
+     * 删除aiSIP外呼网关
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:gateway:remove')")
+    @Log(title = "aiSIP外呼网关", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(aiSipCallGatewayService.deleteAiSipCallGatewayByIds(ids));
+    }
+}

+ 97 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallLlmAgentAccountController.java

@@ -0,0 +1,97 @@
+package com.fs.company.controller.aiSipCall;
+
+import com.fs.aiSipCall.domain.AiSipCallLlmAgentAccount;
+import com.fs.aiSipCall.service.IAiSipCallLlmAgentAccountService;
+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.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼大模型Controller
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@RestController
+@RequestMapping("/company/aiSipCall/llmAgentAccount")
+public class AiSipCallLlmAgentAccountController extends BaseController
+{
+    @Autowired
+    private IAiSipCallLlmAgentAccountService aiSipCallLlmAgentAccountService;
+
+    /**
+     * 查询aiSIP外呼大模型列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:llmAgentAccount:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount)
+    {
+        startPage();
+        List<AiSipCallLlmAgentAccount> list = aiSipCallLlmAgentAccountService.selectAiSipCallLlmAgentAccountList(aiSipCallLlmAgentAccount);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出aiSIP外呼大模型列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:llmAgentAccount:export')")
+    @Log(title = "aiSIP外呼大模型", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount)
+    {
+        List<AiSipCallLlmAgentAccount> list = aiSipCallLlmAgentAccountService.selectAiSipCallLlmAgentAccountList(aiSipCallLlmAgentAccount);
+        ExcelUtil<AiSipCallLlmAgentAccount> util = new ExcelUtil<>(AiSipCallLlmAgentAccount.class);
+        return util.exportExcel(list, "aiSIP外呼大模型数据");
+    }
+
+    /**
+     * 获取aiSIP外呼大模型详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:llmAgentAccount:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(aiSipCallLlmAgentAccountService.selectAiSipCallLlmAgentAccountById(id));
+    }
+
+    /**
+     * 新增aiSIP外呼大模型
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:llmAgentAccount:add')")
+    @Log(title = "aiSIP外呼大模型", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount)
+    {
+        return toAjax(aiSipCallLlmAgentAccountService.insertAiSipCallLlmAgentAccount(aiSipCallLlmAgentAccount));
+    }
+
+    /**
+     * 修改aiSIP外呼大模型
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:llmAgentAccount:edit')")
+    @Log(title = "aiSIP外呼大模型", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount)
+    {
+        return toAjax(aiSipCallLlmAgentAccountService.updateAiSipCallLlmAgentAccount(aiSipCallLlmAgentAccount));
+    }
+
+    /**
+     * 删除aiSIP外呼大模型
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:llmAgentAccount:remove')")
+    @Log(title = "aiSIP外呼大模型", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(aiSipCallLlmAgentAccountService.deleteAiSipCallLlmAgentAccountByIds(ids));
+    }
+}

+ 167 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java

@@ -0,0 +1,167 @@
+package com.fs.company.controller.aiSipCall;
+
+import com.fs.aiSipCall.domain.AiSipCallOutboundCdr;
+import com.fs.aiSipCall.domain.CcCustInfo;
+import com.fs.aiSipCall.param.ApiCallRecordByUuidQueryParams;
+import com.fs.aiSipCall.service.IAiSipCallOutboundCdrService;
+import com.fs.aiSipCall.utils.DateUtils;
+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.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * aiSIP手动外呼通话记录Controller
+ * 
+ * @author fs
+ * @date 2026-03-19
+ */
+@Slf4j
+@RestController
+@RequestMapping("/company/aiSipCall/outboundCdr")
+public class AiSipCallOutboundCdrController extends BaseController
+{
+    @Autowired
+    private IAiSipCallOutboundCdrService aiSipCallOutboundCdrService;
+
+    /**
+     * 查询aiSIP手动外呼通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AiSipCallOutboundCdr aiSipCallOutboundCdr)
+    {
+        startPage();
+        List<AiSipCallOutboundCdr> list = aiSipCallOutboundCdrService.selectAiSipCallOutboundCdrList(aiSipCallOutboundCdr);
+        list.forEach(data -> {
+            data.setStartTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getStartTime())));
+            data.setAnsweredTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getAnsweredTime())));
+            data.setEndTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getEndTime())));
+            data.setTimeLenSec(DateUtils.formatTimeLength(data.getTimeLen()/1000));
+            data.setTimeLenValidStr(DateUtils.formatTimeLength(data.getTimeLenValid()/1000));
+        });
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出aiSIP手动外呼通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:export')")
+    @Log(title = "aiSIP手动外呼通话记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AiSipCallOutboundCdr aiSipCallOutboundCdr)
+    {
+        List<AiSipCallOutboundCdr> list = aiSipCallOutboundCdrService.selectAiSipCallOutboundCdrList(aiSipCallOutboundCdr);
+        ExcelUtil<AiSipCallOutboundCdr> util = new ExcelUtil<>(AiSipCallOutboundCdr.class);
+        return util.exportExcel(list, "aiSIP手动外呼通话记录数据");
+    }
+
+    /**
+     * 获取aiSIP手动外呼通话记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") String id)
+    {
+        return AjaxResult.success(aiSipCallOutboundCdrService.selectAiSipCallOutboundCdrById(id));
+    }
+
+    /**
+     * 新增aiSIP手动外呼通话记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:add')")
+    @Log(title = "aiSIP手动外呼通话记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AiSipCallOutboundCdr aiSipCallOutboundCdr)
+    {
+        return toAjax(aiSipCallOutboundCdrService.insertAiSipCallOutboundCdr(aiSipCallOutboundCdr));
+    }
+
+    /**
+     * 修改aiSIP手动外呼通话记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:edit')")
+    @Log(title = "aiSIP手动外呼通话记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody AiSipCallOutboundCdr aiSipCallOutboundCdr)
+    {
+        return toAjax(aiSipCallOutboundCdrService.updateAiSipCallOutboundCdr(aiSipCallOutboundCdr));
+    }
+
+    /**
+     * 删除aiSIP手动外呼通话记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:remove')")
+    @Log(title = "aiSIP手动外呼通话记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String[] ids)
+    {
+        return toAjax(aiSipCallOutboundCdrService.deleteAiSipCallOutboundCdrByIds(ids));
+    }
+
+    /**
+     * 获取手动外呼客户沟通信息
+     * @param phoneNum 手机号
+     * @param callType 类型  1呼入 2外呼
+     * @param uuid  通话uuid
+     */
+    @GetMapping("/getCustCommunicationInfo")
+    public AjaxResult getCustCommunicationInfo(@RequestParam("phoneNum") String phoneNum, @RequestParam("callType") Integer callType, @RequestParam("uuid") String uuid) {
+        return aiSipCallOutboundCdrService.getCustCommunicationInfo(phoneNum,callType,uuid);
+    }
+    /**
+     * 新增保存手动外呼沟通记录
+     */
+    @PostMapping("/add/custcallrecord")
+    @ResponseBody
+    public AjaxResult addCustcallrecord(@RequestBody CcCustInfo ccCustInfo)
+    {
+        return aiSipCallOutboundCdrService.addCustcallrecord(ccCustInfo);
+    }
+
+
+    /**
+     * 手动拉取今天aiSIP外呼通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:outboundCdr:manualPull')")
+    @GetMapping("/manualPull")
+    public AjaxResult manualPull()
+    {
+        log.info("开始拉取 人工外呼通话记录");
+        long strat = System.currentTimeMillis();
+        String msg = aiSipCallOutboundCdrService.scheduledGetCallRecord().join();
+        log.info("结束拉取 人工外呼通话记录,耗时:{}ms,结果:{}",System.currentTimeMillis()-strat,msg);
+        return AjaxResult.success(msg);
+    }
+
+
+    /**
+     * 同步aiSIP外呼通话记录
+     */
+    @PostMapping("/syncByUuid")
+    public AjaxResult syncByUuid(@RequestBody ApiCallRecordByUuidQueryParams req) {
+        if (req == null || StringUtils.isBlank(req.getUuid())) {
+            return AjaxResult.error("uuid不能为空");
+        }
+        if (StringUtils.isBlank(req.getCallType())) {
+            req.setCallType("03");
+        }
+
+        int rows = aiSipCallOutboundCdrService.syncByUuid(req);
+        if (rows > 0) {
+            return AjaxResult.success("同步成功");
+        }
+        return AjaxResult.error("未查到对应通话记录或同步失败");
+    }
+
+}

+ 163 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallPhoneController.java

@@ -0,0 +1,163 @@
+package com.fs.company.controller.aiSipCall;
+
+import com.fs.aiSipCall.domain.AiSipCallPhone;
+import com.fs.aiSipCall.domain.AiSipCallTask;
+import com.fs.aiSipCall.service.IAiSipCallPhoneService;
+import com.fs.aiSipCall.service.IAiSipCallTaskService;
+import com.fs.aiSipCall.utils.DateUtils;
+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.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * aiSIP外呼通话记录Controller
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Slf4j
+@RestController
+@RequestMapping("/company/aiSipCall/phone")
+public class AiSipCallPhoneController extends BaseController
+{
+    @Autowired
+    private IAiSipCallPhoneService aiSipCallPhoneService;
+    @Autowired
+    private IAiSipCallTaskService aiSipCallTaskService;
+
+    /**
+     * 查询aiSIP外呼通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:phone:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AiSipCallPhone aiSipCallPhone)
+    {
+        startPage();
+        List<AiSipCallPhone> list = aiSipCallPhoneService.selectAiSipCallPhoneList(aiSipCallPhone);
+        Map<Long, String> batchNameMap = new HashMap<>();
+        list.forEach(data -> {
+            String batchName = batchNameMap.getOrDefault(data.getBatchId(), "");
+            if (StringUtils.isBlank(batchName)) {
+                AiSipCallTask ccCallTask = aiSipCallTaskService.selectAiSipCallTaskByRemoteBatchId(data.getBatchId());
+                if (null != ccCallTask) {
+                    batchName = ccCallTask.getBatchName();
+                    data.setBatchId(ccCallTask.getBatchId());
+                } else {
+                    batchName = "非本地任务";
+                }
+                batchNameMap.put(data.getBatchId(), batchName);
+            }
+            data.setBatchName(batchName);
+            data.setCallstatusName( AiSipCallPhone.getCallStatusName(data.getCallstatus()));
+            data.setCalloutTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getCalloutTime())));
+            data.setAnsweredTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getAnsweredTime())));
+            data.setCallEndTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getCallEndTime())));
+            data.setTimeLenSec(DateUtils.formatTimeLength(data.getTimeLen()/1000));
+        });
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出aiSIP外呼通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:phone:export')")
+    @Log(title = "aiSIP外呼通话记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AiSipCallPhone aiSipCallPhone)
+    {
+        List<AiSipCallPhone> list = aiSipCallPhoneService.selectAiSipCallPhoneList(aiSipCallPhone);
+        Map<Long, String> batchNameMap = new HashMap<>();
+        list.forEach(data -> {
+            String batchName = batchNameMap.getOrDefault(data.getBatchId(), "");
+            if (StringUtils.isBlank(batchName)) {
+                AiSipCallTask ccCallTask = aiSipCallTaskService.selectAiSipCallTaskByRemoteBatchId(data.getBatchId());
+                if (null != ccCallTask) {
+                    batchName = ccCallTask.getBatchName();
+                    data.setBatchId(ccCallTask.getBatchId());
+                } else {
+                    batchName = "非本地任务";
+                }
+                batchNameMap.put(data.getBatchId(), batchName);
+            }
+            data.setBatchName(batchName);
+            data.setCallstatusName( AiSipCallPhone.getCallStatusName(data.getCallstatus()));
+            data.setCalloutTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getCalloutTime())));
+            data.setAnsweredTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getAnsweredTime())));
+            data.setCallEndTimeStr(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", new Date(data.getCallEndTime())));
+            data.setTimeLenSec(DateUtils.formatTimeLength(data.getTimeLen()/1000));
+        });
+        ExcelUtil<AiSipCallPhone> util = new ExcelUtil<>(AiSipCallPhone.class);
+        return util.exportExcel(list, "aiSIP外呼通话记录数据");
+    }
+
+    /**
+     * 获取aiSIP外呼通话记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:phone:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") String id)
+    {
+        return AjaxResult.success(aiSipCallPhoneService.selectAiSipCallPhoneById(id));
+    }
+
+    /**
+     * 新增aiSIP外呼通话记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:phone:add')")
+    @Log(title = "aiSIP外呼通话记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AiSipCallPhone aiSipCallPhone)
+    {
+        return toAjax(aiSipCallPhoneService.insertAiSipCallPhone(aiSipCallPhone));
+    }
+
+    /**
+     * 修改aiSIP外呼通话记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:phone:edit')")
+    @Log(title = "aiSIP外呼通话记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody AiSipCallPhone aiSipCallPhone)
+    {
+        return toAjax(aiSipCallPhoneService.updateAiSipCallPhone(aiSipCallPhone));
+    }
+
+    /**
+     * 删除aiSIP外呼通话记录
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:phone:remove')")
+    @Log(title = "aiSIP外呼通话记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String[] ids)
+    {
+        return toAjax(aiSipCallPhoneService.deleteAiSipCallPhoneByIds(ids));
+    }
+
+    /**
+     * 手动拉取今天aiSIP外呼通话记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:phone:manualPull')")
+    @GetMapping("/manualPull")
+    public AjaxResult manualPull()
+    {
+        log.info("开始拉取 Ai 外呼通话记录");
+        long strat = System.currentTimeMillis();
+        String msg = aiSipCallPhoneService.scheduledGetCallRecord().join();
+        log.info("结束拉取 Ai 外呼通话记录,耗时:{}ms,结果:{}",System.currentTimeMillis()-strat,msg);
+        return AjaxResult.success(msg);
+    }
+
+}

+ 257 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallTaskController.java

@@ -0,0 +1,257 @@
+package com.fs.company.controller.aiSipCall;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.aiSipCall.domain.AiSipCallTask;
+import com.fs.aiSipCall.dto.CallTaskStatModel;
+import com.fs.aiSipCall.dto.CommonPhoneModel;
+import com.fs.aiSipCall.service.IAiSipCallTaskService;
+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.poi.ExcelUtil;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * aiSIP外呼任务Controller
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Slf4j
+@RestController
+@RequestMapping("/company/aiSipCall/task")
+public class AiSipCallTaskController extends BaseController
+{
+    @Autowired
+    private IAiSipCallTaskService aiSipCallTaskService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询aiSIP外呼任务列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(AiSipCallTask aiSipCallTask)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        aiSipCallTask.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<AiSipCallTask> list = aiSipCallTaskService.selectAiSipCallTaskList(aiSipCallTask);
+        list.forEach(task -> {
+            CallTaskStatModel statModel = aiSipCallTaskService.statByBatchId(task.getBatchId());
+            task.setPhoneCount(statModel.getPhoneCount());
+            task.setCallCount(statModel.getCallCount());
+            task.setNoCallCount(statModel.getPhoneCount() - statModel.getCallCount());
+            task.setConnectCount(statModel.getConnectCount());
+            task.setNoConnectCount(statModel.getCallCount() - statModel.getConnectCount());
+            if (task.getCallCount() > 0) {
+                task.setRealConnectRate(task.getConnectCount()*1.0/task.getCallCount());
+            } else {
+                task.setRealConnectRate(0.0);
+            }
+        });
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出aiSIP外呼任务列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:export')")
+    @Log(title = "aiSIP外呼任务", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AiSipCallTask aiSipCallTask)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        aiSipCallTask.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AiSipCallTask> list = aiSipCallTaskService.selectAiSipCallTaskList(aiSipCallTask);
+        ExcelUtil<AiSipCallTask> util = new ExcelUtil<>(AiSipCallTask.class);
+        return util.exportExcel(list, "aiSIP外呼任务数据");
+    }
+
+    /**
+     * 获取aiSIP外呼任务详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:query')")
+    @GetMapping(value = "/{batchId}")
+    public AjaxResult getInfo(@PathVariable("batchId") Long batchId)
+    {
+        return AjaxResult.success(aiSipCallTaskService.selectAiSipCallTaskByBatchId(batchId));
+    }
+
+    /**
+     * 新增aiSIP外呼任务
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:add')")
+    @Log(title = "aiSIP外呼任务", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AiSipCallTask aiSipCallTask)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        aiSipCallTask.setCompanyId(loginUser.getCompany().getCompanyId());
+        aiSipCallTask.setCompanyUserId(loginUser.getUser().getUserId());
+        aiSipCallTask.setCreateBy(loginUser.getUser().getUserName());
+        aiSipCallTask.setCreateTime(new Date());
+        // 外呼速率=1/接通率
+        if (null != aiSipCallTask.getConntectRate() && aiSipCallTask.getConntectRate() > 0) {
+            aiSipCallTask.setRate(aiSipCallTask.getConntectRate()/100.0);
+        }
+        return toAjax(aiSipCallTaskService.insertAiSipCallTask(aiSipCallTask));
+    }
+
+    /**
+     * 修改aiSIP外呼任务
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:edit')")
+    @Log(title = "aiSIP外呼任务", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody AiSipCallTask aiSipCallTask)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        aiSipCallTask.setUpdateBy(loginUser.getUser().getUserName());
+        aiSipCallTask.setUpdateTime(new Date());
+        return toAjax(aiSipCallTaskService.updateAiSipCallTask(aiSipCallTask));
+    }
+
+    /**
+     * 删除aiSIP外呼任务
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:remove')")
+    @Log(title = "aiSIP外呼任务", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{batchIds}")
+    public AjaxResult remove(@PathVariable Long[] batchIds)
+    {
+        return toAjax(aiSipCallTaskService.deleteAiSipCallTaskByBatchIds(batchIds));
+    }
+
+    /**
+     * 启动aiSIP外呼任务
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:startTask')")
+    @Log(title = "启动aiSIP外呼任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/startTask/{batchId}")
+    public AjaxResult startTask(@PathVariable Long batchId)
+    {
+        return toAjax(aiSipCallTaskService.startTask(batchId));
+    }
+
+    /**
+     * 暂停aiSIP外呼任务
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:stopTask')")
+    @Log(title = "暂停aiSIP外呼任务", businessType = BusinessType.UPDATE)
+    @PostMapping("/stopTask/{batchId}")
+    public AjaxResult stopTask(@PathVariable Long batchId)
+    {
+        return toAjax(aiSipCallTaskService.stopTask(batchId));
+    }
+
+    /**
+     * 导入外呼号码模板
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:commonImportExcel')")
+    @Log(title = "导入外呼号码", businessType = BusinessType.IMPORT)
+    @PostMapping(value = "/common/importExcel")
+    public AjaxResult commonImportExcel(
+            @RequestParam("batchId") Long batchId,
+            @RequestParam("file") MultipartFile file) throws Exception {
+        List<CommonPhoneModel> phoneList = parseExcelFile(file);
+        return toAjax(aiSipCallTaskService.commonImportExcel(batchId, phoneList));
+    }
+
+    /**
+     * 导入外呼号码数据
+     */
+    @PostMapping(value = "/common/importData/{batchId}")
+    public AjaxResult commonImportData(
+            @PathVariable("batchId") Long batchId,
+            @RequestBody List<CommonPhoneModel> phoneList){
+        return toAjax(aiSipCallTaskService.commonImportExcel(batchId, phoneList));
+    }
+
+    /**
+     * 解析 Excel 文件为电话号码列表
+     */
+    private List<CommonPhoneModel> parseExcelFile(MultipartFile file) throws Exception {
+        List<CommonPhoneModel> phoneList = new ArrayList<>();
+        DataFormatter formatter = new DataFormatter();
+
+        try (InputStream is = file.getInputStream();
+             Workbook workbook = new XSSFWorkbook(is)) {
+
+            Sheet sheet = workbook.getSheetAt(0);
+
+            for (int i = 1; i <= sheet.getLastRowNum(); i++) {
+                Row row = sheet.getRow(i);
+                if (row == null) continue;
+
+                CommonPhoneModel phone = new CommonPhoneModel();
+
+                // 读取客户姓名(第 1 列)
+                Cell nameCell = row.getCell(0);
+                if (nameCell != null) {
+                    String name = formatter.formatCellValue(nameCell);
+                    phone.setNoticeContent(name);
+                    JSONObject bizJson = new JSONObject();
+                    bizJson.put("custName", name);
+                    phone.setBizJson(bizJson);
+                }
+
+                // 读取电话号码(第 2 列)
+                Cell phoneCell = row.getCell(1);
+                if (phoneCell != null) {
+                    phone.setPhoneNum(formatter.formatCellValue(phoneCell));
+                }
+
+                phoneList.add(phone);
+            }
+        }
+        return phoneList;
+    }
+
+    /**
+     * 下载指定类型的模板文件
+     * @param taskType 1 为 ai 外呼
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:task:download:template')")
+    @GetMapping("/download/template/{taskType}")
+    public ResponseEntity<Resource> downloadTemplateByType(@PathVariable("taskType") Integer taskType) {
+        if (taskType == null || taskType != 1) {
+            throw new RuntimeException("不支持的模板类型:" + taskType);
+        }
+
+        String templateName = "AICallList.xlsx";
+        String filePath = "C:\\fs\\AiSipCallTemplate\\" + templateName;
+        Resource resource = new FileSystemResource(filePath);
+
+        if (!resource.exists()) {
+            throw new RuntimeException("模板文件不存在:" + filePath);
+        }
+
+        return ResponseEntity.ok()
+                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + templateName)
+                .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                .body(resource);
+    }
+
+}

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

@@ -0,0 +1,152 @@
+package com.fs.company.controller.aiSipCall;
+
+import com.fs.aiSipCall.domain.AiSipCallUser;
+import com.fs.aiSipCall.service.IAiSipCallUserService;
+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.poi.ExcelUtil;
+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.Date;
+import java.util.List;
+
+/**
+ * sip用户信息Controller
+ * 
+ * @author fs
+ * @date 2026-03-13
+ */
+@RestController
+@RequestMapping("/company/aiSipCall/aiSipCallUser")
+public class AiSipCallUserController extends BaseController
+{
+    @Autowired
+    private IAiSipCallUserService aiSipCallUserService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询sip用户信息列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:aiSipCallUser:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AiSipCallUser aiSipCallUser)
+    {
+        startPage();
+        List<AiSipCallUser> list = aiSipCallUserService.selectAiSipCallUserList(aiSipCallUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出sip用户信息列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:aiSipCallUser:export')")
+    @Log(title = "sip用户信息", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AiSipCallUser aiSipCallUser)
+    {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        aiSipCallUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AiSipCallUser> list = aiSipCallUserService.selectAiSipCallUserList(aiSipCallUser);
+        ExcelUtil<AiSipCallUser> util = new ExcelUtil<>(AiSipCallUser.class);
+        return util.exportExcel(list, "sip用户信息数据");
+    }
+
+    /**
+     * 获取sip用户信息详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:aiSipCallUser:query')")
+    @GetMapping(value = "/{userId}")
+    public AjaxResult getInfo(@PathVariable("userId") Long userId)
+    {
+        return AjaxResult.success(aiSipCallUserService.selectAiSipCallUserByUserId(userId));
+    }
+
+    /**
+     * 新增sip用户信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:aiSipCallUser:add')")
+    @Log(title = "sip用户信息", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AiSipCallUser aiSipCallUser)
+    {
+        if(aiSipCallUser.getCompanyUserId() == null){
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            aiSipCallUser.setCompanyId(loginUser.getCompany().getCompanyId());
+            aiSipCallUser.setCompanyUserId(loginUser.getUser().getUserId());
+            aiSipCallUser.setCreateBy(loginUser.getUser().getUserName());
+        }
+        aiSipCallUser.setCreateTime(new Date());
+        return toAjax(aiSipCallUserService.insertAiSipCallUser(aiSipCallUser));
+    }
+
+    /**
+     * 修改sip用户信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:aiSipCallUser:edit')")
+    @Log(title = "sip用户信息", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody AiSipCallUser aiSipCallUser)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        aiSipCallUser.setUpdateBy(loginUser.getUser().getUserName());
+        aiSipCallUser.setUpdateTime(new Date());
+        return toAjax(aiSipCallUserService.updateAiSipCallUser(aiSipCallUser));
+    }
+
+    /**
+     * 删除sip用户信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:aiSipCallUser:remove')")
+    @Log(title = "sip用户信息", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{userIds}")
+    public AjaxResult remove(@PathVariable Long[] userIds)
+    {
+        return toAjax(aiSipCallUserService.deleteAiSipCallUserByUserIds(userIds));
+    }
+
+    /**
+     * 获取未绑定的分机列表
+     */
+    @GetMapping("/getUnBindExtnum")
+    public AjaxResult getUnBindExtnum()
+    {
+        return aiSipCallUserService.getUnBindExtnum();
+    }
+
+    /**
+     * 查询登录销售的sip用户信息列表
+     */
+    @GetMapping("/myCallUser")
+    public AjaxResult myCallUser(AiSipCallUser aiSipCallUser)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        aiSipCallUser.setCompanyUserId(loginUser.getUser().getUserId());
+        startPage();
+        List<AiSipCallUser> list = aiSipCallUserService.selectAiSipCallUserList(aiSipCallUser);
+        if(!list.isEmpty()){
+            return AjaxResult.success(list.get(0));
+        }else{
+            return AjaxResult.error("未创建sip角色");
+        }
+    }
+
+    /**
+     * 查询aiSIP工具条基础配置参数
+     * @param extNum 分机号
+     * @return AjaxResult 结果
+     */
+    @GetMapping("/getToolbarBasicParam/{extNum}")
+    public AjaxResult getToolbarBasicParam(@PathVariable("extNum") String extNum)
+    {
+        return aiSipCallUserService.getToolbarBasicParam(extNum);
+    }
+}

+ 97 - 0
fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallVoiceTtsAliyunController.java

@@ -0,0 +1,97 @@
+package com.fs.company.controller.aiSipCall;
+
+import com.fs.aiSipCall.domain.AiSipCallVoiceTtsAliyun;
+import com.fs.aiSipCall.service.IAiSipCallVoiceTtsAliyunService;
+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.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼阿里云音色Controller
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@RestController
+@RequestMapping("/company/aiSipCall/voiceTtsAliyun")
+public class AiSipCallVoiceTtsAliyunController extends BaseController
+{
+    @Autowired
+    private IAiSipCallVoiceTtsAliyunService aiSipCallVoiceTtsAliyunService;
+
+    /**
+     * 查询aiSIP外呼阿里云音色列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:voiceTtsAliyun:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun)
+    {
+        startPage();
+        List<AiSipCallVoiceTtsAliyun> list = aiSipCallVoiceTtsAliyunService.selectAiSipCallVoiceTtsAliyunList(aiSipCallVoiceTtsAliyun);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出aiSIP外呼阿里云音色列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:voiceTtsAliyun:export')")
+    @Log(title = "aiSIP外呼阿里云音色", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun)
+    {
+        List<AiSipCallVoiceTtsAliyun> list = aiSipCallVoiceTtsAliyunService.selectAiSipCallVoiceTtsAliyunList(aiSipCallVoiceTtsAliyun);
+        ExcelUtil<AiSipCallVoiceTtsAliyun> util = new ExcelUtil<>(AiSipCallVoiceTtsAliyun.class);
+        return util.exportExcel(list, "aiSIP外呼阿里云音色数据");
+    }
+
+    /**
+     * 获取aiSIP外呼阿里云音色详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:voiceTtsAliyun:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(aiSipCallVoiceTtsAliyunService.selectAiSipCallVoiceTtsAliyunById(id));
+    }
+
+    /**
+     * 新增aiSIP外呼阿里云音色
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:voiceTtsAliyun:add')")
+    @Log(title = "aiSIP外呼阿里云音色", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun)
+    {
+        return toAjax(aiSipCallVoiceTtsAliyunService.insertAiSipCallVoiceTtsAliyun(aiSipCallVoiceTtsAliyun));
+    }
+
+    /**
+     * 修改aiSIP外呼阿里云音色
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:voiceTtsAliyun:edit')")
+    @Log(title = "aiSIP外呼阿里云音色", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun)
+    {
+        return toAjax(aiSipCallVoiceTtsAliyunService.updateAiSipCallVoiceTtsAliyun(aiSipCallVoiceTtsAliyun));
+    }
+
+    /**
+     * 删除aiSIP外呼阿里云音色
+     */
+    @PreAuthorize("@ss.hasPermi('company:aiSipCall:voiceTtsAliyun:remove')")
+    @Log(title = "aiSIP外呼阿里云音色", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(aiSipCallVoiceTtsAliyunService.deleteAiSipCallVoiceTtsAliyunByIds(ids));
+    }
+}

+ 357 - 0
fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentAccountController.java

@@ -0,0 +1,357 @@
+package com.fs.company.controller.aicall;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.aicall.domain.CcCallTask;
+import com.fs.aicall.domain.CcLlmAgentAccount;
+import com.fs.aicall.service.ICcCallTaskService;
+import com.fs.aicall.service.ICcLlmAgentAccountService;
+import com.fs.aicall.service.ICcParamsService;
+import com.fs.aicall.service.ICompanyBindAiModelService;
+import com.fs.aicall.utils.CommonUtils;
+import com.fs.aicall.utils.StringUtils;
+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.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 机器人参数配置Controller
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Controller
+@RequestMapping("/aicall/account")
+public class CcLlmAgentAccountController extends BaseController
+{
+    private String prefix = "aicall/account";
+
+    @Autowired
+    private ICcLlmAgentAccountService ccLlmAgentAccountService;
+    @Autowired
+    private ICcParamsService ccParamsService;
+    @Autowired
+    private ICcCallTaskService ccCallTaskService;
+    @Autowired
+    private ICompanyBindAiModelService companyBindAiModelService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    SysConfigMapper sysConfigMapper;
+
+    private static List<String> hideKeys = Arrays.asList("apiKey", "oauthPrivateKey", "oauthPublicKeyId", "patToken");
+
+
+    @PreAuthorize("@ss.hasPermi('aicall:account:view')")
+    @GetMapping()
+    public String account()
+    {
+        return prefix + "/account";
+    }
+
+    /**
+     * 查询机器人参数配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:list')")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(@RequestBody CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        // 获取当前登录的公司ID
+        Long companyId = getCurrentCompanyId();
+        if (companyId != null) {
+            List<Long> modelIds = companyBindAiModelService.selectModelIdsByCompanyId(companyId);
+            if (!modelIds.isEmpty()) {
+                ccLlmAgentAccount.setModelIds(modelIds);
+            } else {
+                return getDataTable(new ArrayList<>());
+            }
+        }
+        
+        startPage();
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+        TableDataInfo tableDataInfo = getDataTable(list);
+        List<CcLlmAgentAccount> records = (List<CcLlmAgentAccount>) tableDataInfo.getRows();
+        for (CcLlmAgentAccount data: records) {
+            JSONObject accountJson = JSONObject.parseObject(data.getAccountJson());
+            for (String key: accountJson.keySet()) {
+                if (hideKeys.contains(key)) {
+                    accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+                }
+            }
+            data.setAccountJson(JSONObject.toJSONString(accountJson));
+        }
+        tableDataInfo.setRows(records);
+        return tableDataInfo;
+    }
+
+    /**
+     * 获取当前登录的公司ID
+     * @return 公司ID
+     */
+    private Long getCurrentCompanyId() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return loginUser.getUser().getCompanyId();
+    }
+
+    /**
+     * 导出机器人参数配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:export')")
+    @Log(title = "机器人参数配置", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(ccLlmAgentAccount);
+        ExcelUtil<CcLlmAgentAccount> util = new ExcelUtil<CcLlmAgentAccount>(CcLlmAgentAccount.class);
+        return util.exportExcel(list, "机器人参数配置数据");
+    }
+
+    /**
+     * 新增机器人参数配置
+     */
+    @GetMapping("/add")
+    public String add(ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = new CcLlmAgentAccount();
+        ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        ccLlmAgentAccount.setInterruptFlag(0);
+        ccLlmAgentAccount.setAccountJson("{}");
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:add')")
+    @Log(title = "机器人参数配置", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(@RequestBody CcLlmAgentAccount ccLlmAgentAccount)
+    {
+        if ("Coze".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("LocalNlpChat".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("JiutianWorkflow".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())
+                || "JiutianAgent".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("JiutianAccount");
+        } else {
+            ccLlmAgentAccount.setAccountEntity("LlmAccount");
+        }
+        if (null == ccLlmAgentAccount.getKbCatId()) {
+            ccLlmAgentAccount.setKbCatId(-1);
+        }
+        if("DeepSeekChat".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())){
+            JSONObject jsonObject = JSONObject.parseObject( ccLlmAgentAccount.getAccountJson());
+            if(null != jsonObject && jsonObject.containsKey("apiKey") && jsonObject.getString("apiKey").contains("**")){
+                SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("cId.config");
+                if(null != sysConfig && StringUtils.isNotBlank(sysConfig.getConfigValue())){
+                    JSONObject configValue = JSONObject.parseObject(sysConfig.getConfigValue());
+                    jsonObject.put("apiKey", configValue.getString("apiKey"));
+                    ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(jsonObject));
+                }
+            }
+        }
+        
+        // 新增模型
+        int result = ccLlmAgentAccountService.insertCcLlmAgentAccount(ccLlmAgentAccount);
+        
+        // 获取当前登录的公司ID
+        Long companyId = getCurrentCompanyId();
+        if (result > 0 && companyId != null) {
+            companyBindAiModelService.bindCompanyToModel(ccLlmAgentAccount.getId().longValue(), companyId);
+        }
+        
+        return toAjax(result);
+    }
+
+    /**
+     * 修改机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:edit')")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(id);
+        if (StringUtils.isBlank(ccLlmAgentAccount.getInterruptIgnoreKeywords())) {
+            ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        }
+        JSONObject accountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: accountJson.keySet()) {
+            if (hideKeys.contains(key)) {
+                accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(accountJson));
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+
+        String errMsg = checkEdit(ccLlmAgentAccount.getId());
+        mmap.put("errorMsg", errMsg);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 复制机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:edit')")
+    @GetMapping("/copy/{id}")
+    public String copy(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentAccount ccLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(id);
+        if (StringUtils.isBlank(ccLlmAgentAccount.getInterruptIgnoreKeywords())) {
+            ccLlmAgentAccount.setInterruptIgnoreKeywords(ccParamsService.getParamValueByCode("default_interrupt_ignore_keywords", ""));
+        }
+        ccLlmAgentAccount.setId(-1*ccLlmAgentAccount.getId());
+        ccLlmAgentAccount.setName(ccLlmAgentAccount.getName() + "-副本");
+        JSONObject accountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: accountJson.keySet()) {
+            if (hideKeys.contains(key)) {
+                accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(accountJson));
+        mmap.put("ccLlmAgentAccount", ccLlmAgentAccount);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:edit')")
+    @Log(title = "机器人参数配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(@RequestBody CcLlmAgentAccount ccLlmAgentAccount)
+    {
+
+        if (ccLlmAgentAccount.getId() > 0) {
+            String errMsg = checkEdit(ccLlmAgentAccount.getId());
+            if (StringUtils.isNotEmpty(errMsg)) {
+                return AjaxResult.error(errMsg);
+            }
+        }
+
+        if ("Coze".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("LocalNlpChat".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("CozeAccount");
+        } else if ("JiutianWorkflow".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())
+                || "JiutianAgent".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())) {
+            ccLlmAgentAccount.setAccountEntity("JiutianAccount");
+        } else {
+            ccLlmAgentAccount.setAccountEntity("LlmAccount");
+        }
+
+        Integer orignId = ccLlmAgentAccount.getId();
+        if (orignId < 0) {
+            orignId = orignId * -1;
+        }
+        CcLlmAgentAccount oldCcLlmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(orignId);
+        JSONObject oldAccountJson = JSONObject.parseObject(oldCcLlmAgentAccount.getAccountJson());
+        JSONObject newAccountJson = JSONObject.parseObject(ccLlmAgentAccount.getAccountJson());
+        for (String key: newAccountJson.keySet()) {
+            // 是需要脱敏的key,且值包含星号,则值不更新
+            if (hideKeys.contains(key) && newAccountJson.getString(key).contains("****")) {
+                newAccountJson.put(key, oldAccountJson.getString(key));
+            }
+        }
+        ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(newAccountJson));
+        if (null == ccLlmAgentAccount.getKbCatId()) {
+            ccLlmAgentAccount.setKbCatId(-1);
+        }
+
+        if (ccLlmAgentAccount.getId() > 0) {
+            return toAjax(ccLlmAgentAccountService.updateCcLlmAgentAccount(ccLlmAgentAccount));
+        } else {
+            ccLlmAgentAccount.setId(null);
+            return toAjax(ccLlmAgentAccountService.insertCcLlmAgentAccount(ccLlmAgentAccount));
+        }
+    }
+
+    /**
+     * 删除机器人参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:account:remove')")
+    @Log(title = "机器人参数配置", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        //获取公司id
+        Long companyId = getCurrentCompanyId();
+        //删除公司绑定的AI模型
+        companyBindAiModelService.deleteBindAiModelByCompanyIdAndModelIds(companyId,ids);
+
+        return toAjax(ccLlmAgentAccountService.deleteCcLlmAgentAccountByIds(ids));
+    }
+
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcLlmAgentAccount> list = ccLlmAgentAccountService.selectCcLlmAgentAccountList(new CcLlmAgentAccount());
+        for (CcLlmAgentAccount data: list) {
+            JSONObject accountJson = JSONObject.parseObject(data.getAccountJson());
+            for (String key: accountJson.keySet()) {
+                if (hideKeys.contains(key)) {
+                    accountJson.put(key, CommonUtils.maskStringUtil(accountJson.getString(key)));
+                }
+            }
+            data.setAccountJson(JSONObject.toJSONString(accountJson));
+        }
+        return AjaxResult.success(list);
+    }
+
+
+
+    private String checkEdit(Integer id)
+    {
+        CcLlmAgentAccount llmAgentAccount = ccLlmAgentAccountService.selectCcLlmAgentAccountById(id);
+        List<CcCallTask> ccCallTaskList = ccCallTaskService.selectCcCallTaskList(new CcCallTask().setLlmAccountId(id));
+        List<String> ids = new ArrayList<>();
+        for (CcCallTask ccCallTask: ccCallTaskList) {
+            // AI外呼,且不自动停止,且状态为正在拨打的任务,需要手动停止任务后再修改大模型配置再手动启动任务
+            if (ccCallTask.getTaskType() == 1
+                    && ccCallTask.getIfcall() == 1
+                    && ccCallTask.getAutoStop() == 0) {
+                ids.add(ccCallTask.getBatchName());
+            }
+        }
+        if (ids.size() > 0) {
+            return String.format("%s%s%s", "请先暂停任务:", StringUtils.join(ids, ","), ",再修改该配置,修改完成后再启动任务");
+        } else {
+            return "";
+        }
+    }
+
+    @GetMapping("/getCidConfig")
+    @ResponseBody
+    public R getCidConfig()
+    {
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("cId.config");
+        return R.ok().put("data",sysConfig.getConfigValue());
+    }
+}

+ 134 - 0
fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentProviderController.java

@@ -0,0 +1,134 @@
+package com.fs.company.controller.aicall;
+
+
+import com.fs.aicall.domain.CcLlmAgentProvider;
+import com.fs.aicall.service.ICcLlmAgentProviderService;
+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.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 大模型实现类列表Controller
+ * 
+ * @author ruoyi
+ * @date 2025-06-16
+ */
+@Controller
+@RequestMapping("/aicall/provider")
+public class CcLlmAgentProviderController extends BaseController
+{
+    private String prefix = "aicall/provider";
+
+    @Autowired
+    private ICcLlmAgentProviderService ccLlmAgentProviderService;
+
+    @PreAuthorize("@ss.hasPermi('aicall:provider:view')")
+    @GetMapping()
+    public String provider()
+    {
+        return prefix + "/provider";
+    }
+
+    /**
+     * 查询大模型实现类列表列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:list')")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        startPage();
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(ccLlmAgentProvider);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出大模型实现类列表列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:export')")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(ccLlmAgentProvider);
+        ExcelUtil<CcLlmAgentProvider> util = new ExcelUtil<CcLlmAgentProvider>(CcLlmAgentProvider.class);
+        return util.exportExcel(list, "大模型实现类列表数据");
+    }
+
+    /**
+     * 新增大模型实现类列表
+     */
+    @GetMapping("/add")
+    public String add()
+    {
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存大模型实现类列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:add')")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return toAjax(ccLlmAgentProviderService.insertCcLlmAgentProvider(ccLlmAgentProvider));
+    }
+
+    /**
+     * 修改大模型实现类列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:edit')")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Integer id, ModelMap mmap)
+    {
+        CcLlmAgentProvider ccLlmAgentProvider = ccLlmAgentProviderService.selectCcLlmAgentProviderById(id);
+        mmap.put("ccLlmAgentProvider", ccLlmAgentProvider);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存大模型实现类列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:edit')")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcLlmAgentProvider ccLlmAgentProvider)
+    {
+        return toAjax(ccLlmAgentProviderService.updateCcLlmAgentProvider(ccLlmAgentProvider));
+    }
+
+    /**
+     * 删除大模型实现类列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:provider:remove')")
+    @Log(title = "大模型实现类列表", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccLlmAgentProviderService.deleteCcLlmAgentProviderByIds(ids));
+    }
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcLlmAgentProvider> list = ccLlmAgentProviderService.selectCcLlmAgentProviderList(new CcLlmAgentProvider());
+        return AjaxResult.success(list);
+    }
+
+}

+ 153 - 0
fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmKbCatController.java

@@ -0,0 +1,153 @@
+package com.fs.company.controller.aicall;
+
+import com.fs.aicall.domain.CcLlmKbCat;
+import com.fs.aicall.service.ICcLlmKbCatService;
+import com.fs.aicall.service.ICcLlmKbService;
+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.poi.ExcelUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 知识库Controller
+ * 
+ * @author ruoyi
+ * @date 2026-01-19
+ */
+@Controller
+@RequestMapping("/aicall/kbcat")
+public class CcLlmKbCatController extends BaseController
+{
+    private String prefix = "aicall/kbcat";
+
+    @Autowired
+    private ICcLlmKbCatService ccLlmKbCatService;
+    @Autowired
+    private ICcLlmKbService ccLlmKbService;
+
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:view')")
+    @GetMapping()
+    public String kbcat()
+    {
+        return prefix + "/kbcat";
+    }
+
+    /**
+     * 查询知识库列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:list')")
+    @PostMapping("/list")
+    @ResponseBody
+    public TableDataInfo list(CcLlmKbCat ccLlmKbCat)
+    {
+        startPage();
+        List<CcLlmKbCat> list = ccLlmKbCatService.selectCcLlmKbCatList(ccLlmKbCat);
+        for (CcLlmKbCat data: list) {
+            data.setContentCount(ccLlmKbService.selectCountByCatId(data.getId()));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出知识库列表
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:export')")
+    @Log(title = "知识库", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    @ResponseBody
+    public AjaxResult export(CcLlmKbCat ccLlmKbCat)
+    {
+        List<CcLlmKbCat> list = ccLlmKbCatService.selectCcLlmKbCatList(ccLlmKbCat);
+        ExcelUtil<CcLlmKbCat> util = new ExcelUtil<CcLlmKbCat>(CcLlmKbCat.class);
+        return util.exportExcel(list, "知识库数据");
+    }
+
+    /**
+     * 新增知识库
+     */
+    @GetMapping("/add")
+    public String add(ModelMap mmap)
+    {
+        mmap.put("ccLlmKbCat", new CcLlmKbCat());
+        return prefix + "/add";
+    }
+
+    /**
+     * 新增保存知识库
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:add')")
+    @Log(title = "知识库", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    @ResponseBody
+    public AjaxResult addSave(CcLlmKbCat ccLlmKbCat)
+    {
+        CcLlmKbCat checkCcLlmKbCat = ccLlmKbCatService.selectCcLlmKbCatByCat(null, ccLlmKbCat.getCat());
+        if (null != checkCcLlmKbCat) {
+            return AjaxResult.error("知识库分类不能重复,请修改");
+        }
+        return toAjax(ccLlmKbCatService.insertCcLlmKbCat(ccLlmKbCat));
+    }
+
+    /**
+     * 修改知识库
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:edit')")
+    @GetMapping("/edit/{id}")
+    public String edit(@PathVariable("id") Long id, ModelMap mmap)
+    {
+        CcLlmKbCat ccLlmKbCat = ccLlmKbCatService.selectCcLlmKbCatById(id);
+        mmap.put("ccLlmKbCat", ccLlmKbCat);
+        return prefix + "/edit";
+    }
+
+    /**
+     * 修改保存知识库
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:edit')")
+    @Log(title = "知识库", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    @ResponseBody
+    public AjaxResult editSave(CcLlmKbCat ccLlmKbCat)
+    {
+        CcLlmKbCat checkCcLlmKbCat = ccLlmKbCatService.selectCcLlmKbCatByCat(ccLlmKbCat.getId(), ccLlmKbCat.getCat());
+        if (null != checkCcLlmKbCat) {
+            return AjaxResult.error("知识库分类不能重复,请修改");
+        }
+        return toAjax(ccLlmKbCatService.updateCcLlmKbCat(ccLlmKbCat));
+    }
+
+    /**
+     * 删除知识库
+     */
+    @PreAuthorize("@ss.hasPermi('aicall:kbcat:remove')")
+    @Log(title = "知识库", businessType = BusinessType.DELETE)
+    @PostMapping( "/remove")
+    @ResponseBody
+    public AjaxResult remove(String ids)
+    {
+        return toAjax(ccLlmKbCatService.deleteCcLlmKbCatByIds(ids));
+    }
+
+
+
+
+    @GetMapping("/all")
+    @ResponseBody
+    public AjaxResult all()
+    {
+        List<CcLlmKbCat> list = ccLlmKbCatService.selectCcLlmKbCatList(new CcLlmKbCat());
+        return AjaxResult.success(list);
+    }
+
+
+
+}

+ 121 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowServerController.java

@@ -0,0 +1,121 @@
+package com.fs.company.controller.company;
+
+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.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyAiWorkflowServer;
+import com.fs.company.param.BindCidServerParam;
+import com.fs.company.service.ICompanyAiWorkflowServerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * cid服务Controller
+ * 
+ * @author fs
+ * @date 2026-02-26
+ */
+@RestController
+@RequestMapping("/company/cid/server")
+public class CompanyAiWorkflowServerController extends BaseController
+{
+    @Autowired
+    private ICompanyAiWorkflowServerService companyAiWorkflowServerService;
+
+    /**
+     * 查询cid服务列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyAiWorkflowServer companyAiWorkflowServer)
+    {
+        startPage();
+        List<CompanyAiWorkflowServer> list = companyAiWorkflowServerService.selectCompanyAiWorkflowServerList(companyAiWorkflowServer);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出cid服务列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:export')")
+    @Log(title = "cid服务", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyAiWorkflowServer companyAiWorkflowServer)
+    {
+        List<CompanyAiWorkflowServer> list = companyAiWorkflowServerService.selectCompanyAiWorkflowServerList(companyAiWorkflowServer);
+        ExcelUtil<CompanyAiWorkflowServer> util = new ExcelUtil<CompanyAiWorkflowServer>(CompanyAiWorkflowServer.class);
+        return util.exportExcel(list, "cid服务数据");
+    }
+
+    /**
+     * 获取cid服务详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(companyAiWorkflowServerService.selectCompanyAiWorkflowServerById(id));
+    }
+
+    /**
+     * 新增cid服务
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:add')")
+    @Log(title = "cid服务", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyAiWorkflowServer companyAiWorkflowServer)
+    {
+        return toAjax(companyAiWorkflowServerService.insertCompanyAiWorkflowServer(companyAiWorkflowServer));
+    }
+
+    /**
+     * 修改cid服务
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:edit')")
+    @Log(title = "cid服务", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CompanyAiWorkflowServer companyAiWorkflowServer)
+    {
+        return toAjax(companyAiWorkflowServerService.updateCompanyAiWorkflowServer(companyAiWorkflowServer));
+    }
+
+    /**
+     * 删除cid服务
+     */
+    @PreAuthorize("@ss.hasPermi('company:server:remove')")
+    @Log(title = "cid服务", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(companyAiWorkflowServerService.deleteCompanyAiWorkflowServerByIds(ids));
+    }
+
+    /**
+     * 绑定cid服务
+     * @param param
+     * @return
+     */
+    @PostMapping("/bindCidServer")
+    public R bindCidServer(@RequestBody BindCidServerParam param){
+        return companyAiWorkflowServerService.bindCidServer(param);
+    }
+
+    /**
+     * 解绑cid服务
+     * @param param
+     * @return
+     */
+    @PostMapping("/unbindCidServer")
+    public R unbindCidServer(@RequestBody BindCidServerParam param){
+        return companyAiWorkflowServerService.unbindCidServer(param);
+    }
+
+
+}

+ 111 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java

@@ -0,0 +1,111 @@
+package com.fs.company.controller.company;
+
+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.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRoboticCallBlacklist;
+import com.fs.company.service.ICompanyVoiceRoboticCallBlacklistService;
+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.List;
+
+/**
+ * 黑名单Controller
+ *
+ * @author fs
+ * @date 2023-02-23
+ */
+@RestController
+@RequestMapping("/company/companyVoiceRoboticCallBlacklist")
+public class CompanyVoiceRoboticCallBlacklistController extends BaseController
+{
+    @Autowired
+    private ICompanyVoiceRoboticCallBlacklistService companyVoiceRoboticCallBlacklistService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询黑名单列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRoboticCallBlacklist.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<CompanyVoiceRoboticCallBlacklist> list = companyVoiceRoboticCallBlacklistService.selectCompanyVoiceRoboticCallBlacklistList(companyVoiceRoboticCallBlacklist);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出黑名单列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:export')")
+    @Log(title = "黑名单", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
+    {
+        List<CompanyVoiceRoboticCallBlacklist> list = companyVoiceRoboticCallBlacklistService.selectCompanyVoiceRoboticCallBlacklistList(companyVoiceRoboticCallBlacklist);
+        ExcelUtil<CompanyVoiceRoboticCallBlacklist> util = new ExcelUtil<CompanyVoiceRoboticCallBlacklist>(CompanyVoiceRoboticCallBlacklist.class);
+        return util.exportExcel(list, "companyVoiceBlacklist");
+    }
+
+    /**
+     * 获取黑名单详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:query')")
+    @GetMapping(value = "/{blacklistId}")
+    public AjaxResult getInfo(@PathVariable("blacklistId") Long blacklistId)
+    {
+        return AjaxResult.success(companyVoiceRoboticCallBlacklistService.selectCompanyVoiceRoboticCallBlacklistById(blacklistId));
+    }
+
+    /**
+     * 新增黑名单
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:add')")
+    @Log(title = "黑名单", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
+    {
+        return toAjax(companyVoiceRoboticCallBlacklistService.insertCompanyVoiceRoboticCallBlacklist(companyVoiceRoboticCallBlacklist));
+    }
+
+    /**
+     * 修改黑名单nessType = BusinessType.UPDATE)
+     * */
+    @PutMapping
+    @Log(title = "修改黑名单信息", businessType = BusinessType.INSERT)
+    public AjaxResult edit(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
+    {
+        return toAjax(companyVoiceRoboticCallBlacklistService.updateCompanyVoiceRoboticCallBlacklist(companyVoiceRoboticCallBlacklist));
+    }
+
+    /**
+     * 删除黑名单
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:remove')")
+    @Log(title = "黑名单", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{blacklistIds}")
+    public AjaxResult remove(@PathVariable Long[] blacklistIds)
+    {
+        return toAjax(companyVoiceRoboticCallBlacklistService.deleteCompanyVoiceRoboticCallBlacklistByIds(blacklistIds));
+    }
+
+    @Log(title = "修改黑名单状态", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist) {
+        return toAjax(companyVoiceRoboticCallBlacklistService.changeStatus(companyVoiceRoboticCallBlacklist));
+    }
+
+
+}

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

@@ -0,0 +1,175 @@
+package com.fs.company.controller.company;
+
+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.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogAddwx;
+import com.fs.company.domain.CompanyWxClient;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogAddwxService;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogAddWxExportVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogAddwxVO;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 调用日志_加微信Controller
+ * 
+ * @author fs
+ * @date 2026-01-15
+ */
+@RestController
+@RequestMapping("/company/addwxLog")
+public class CompanyVoiceRoboticCallLogAddwxController extends BaseController
+{
+    @Autowired
+    private ICompanyVoiceRoboticCallLogAddwxService companyVoiceRoboticCallLogAddwxService;
+
+    /**
+     * 查询调用日志_加微信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:addwxlog:list')")
+    @GetMapping("/list")
+    public TableDataInfo listByCustomerIdAndRoboticId(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+    {
+//        if( null == companyVoiceRoboticCallLogAddwx.getCustomerId() || null == companyVoiceRoboticCallLogAddwx.getRoboticId()){
+//            return getDataTable(null);
+//        }
+        //查询用户
+        if(null == companyVoiceRoboticCallLogAddwx.getRoboticId()){
+            List<CompanyWxClient> wxclients = companyVoiceRoboticCallLogAddwxService.getWxClientInfoByCustomerId(companyVoiceRoboticCallLogAddwx.getCustomerId());
+            if(null != wxclients && !wxclients.isEmpty()){
+                Set<Long> collect = wxclients.stream().map(e -> e.getId()).collect(Collectors.toSet());
+                companyVoiceRoboticCallLogAddwx.setWxclientIds(new ArrayList<>( collect));
+                startPage();
+                List<CompanyVoiceRoboticCallLogAddwxVO> companyVoiceRoboticCallLogAddwxVOS = companyVoiceRoboticCallLogAddwxService.listByCustomerId(companyVoiceRoboticCallLogAddwx);
+                return getDataTable(companyVoiceRoboticCallLogAddwxVOS);
+            } else{
+                return getDataTable(Collections.EMPTY_LIST);
+            }
+        }
+        else {
+            CompanyWxClient wxClient = companyVoiceRoboticCallLogAddwxService.getWxClientInfoByCustomerIdAndRoboticId(companyVoiceRoboticCallLogAddwx.getCustomerId(), companyVoiceRoboticCallLogAddwx.getRoboticId());
+            companyVoiceRoboticCallLogAddwx.setWxClientId(wxClient.getId());
+            startPage();
+            List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listByCustomerIdAndRoboticId(companyVoiceRoboticCallLogAddwx);
+            return getDataTable(list);
+        }
+
+    }
+
+    /**
+     * 查询调用日志_加微信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:addwxlog:list')")
+    @GetMapping("/listAll")
+    public TableDataInfo listAll(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listAll(companyVoiceRoboticCallLogAddwx);
+        TableDataInfo dataTable = getDataTable(list);
+        Map<String, Long> countMap = companyVoiceRoboticCallLogAddwxService.countListAll(companyVoiceRoboticCallLogAddwx);
+        if (countMap != null) {
+            dataTable.setTotalRecordCount(countMap.getOrDefault("totalRecordCount", 0L));
+            dataTable.setSuccessRecordCount(countMap.getOrDefault("successRecordCount", 0L));
+            dataTable.setTodayRecordCount(countMap.getOrDefault("todayRecordCount", 0L));
+        }
+        return dataTable;
+
+    }
+
+
+    /**
+     * 导出调用日志_加微信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:addwxlog:export')")
+    @Log(title = "调用日志_加微信", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+    {
+        List<CompanyVoiceRoboticCallLogAddwxVO> list = companyVoiceRoboticCallLogAddwxService.listAll(companyVoiceRoboticCallLogAddwx);
+        List<CompanyVoiceRoboticCallLogAddWxExportVO> exportList = list.stream().map(item -> {
+            CompanyVoiceRoboticCallLogAddWxExportVO vo = new CompanyVoiceRoboticCallLogAddWxExportVO();
+            BeanUtils.copyProperties(item, vo);
+            return vo;
+        }).collect(Collectors.toList());
+        ExcelUtil<CompanyVoiceRoboticCallLogAddWxExportVO> util = new ExcelUtil<CompanyVoiceRoboticCallLogAddWxExportVO>(CompanyVoiceRoboticCallLogAddWxExportVO.class);
+        return util.exportExcel(exportList, "调用日志_加微信数据");
+    }
+
+    /**
+     * 删除调用日志_加微信
+     */
+    @PreAuthorize("@ss.hasPermi('company:addwxlog:remove')")
+    @Log(title = "调用日志_加微信", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{logIds}")
+    public AjaxResult remove(@PathVariable Long[] logIds)
+    {
+        return toAjax(companyVoiceRoboticCallLogAddwxService.deleteCompanyVoiceRoboticCallLogAddwxByLogIds(logIds));
+    }
+
+//    /**
+//     * 导出调用日志_加微信列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:addwxlog:export')")
+//    @Log(title = "调用日志_加微信", businessType = BusinessType.EXPORT)
+//    @GetMapping("/export")
+//    public AjaxResult export(CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+//    {
+//        List<CompanyVoiceRoboticCallLogAddwx> list = companyVoiceRoboticCallLogAddwxService.selectCompanyVoiceRoboticCallLogAddwxList(companyVoiceRoboticCallLogAddwx);
+//        ExcelUtil<CompanyVoiceRoboticCallLogAddwx> util = new ExcelUtil<CompanyVoiceRoboticCallLogAddwx>(CompanyVoiceRoboticCallLogAddwx.class);
+//        return util.exportExcel(list, "调用日志_加微信数据");
+//    }
+//
+
+//
+//    /**
+//     * 获取调用日志_加微信详细信息
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:addwxlog:query')")
+//    @GetMapping(value = "/{logId}")
+//    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+//    {
+//        return AjaxResult.success(companyVoiceRoboticCallLogAddwxService.selectCompanyVoiceRoboticCallLogAddwxByLogId(logId));
+//    }
+//
+//    /**
+//     * 新增调用日志_加微信
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:addwxlog:add')")
+//    @Log(title = "调用日志_加微信", businessType = BusinessType.INSERT)
+//    @PostMapping
+//    public AjaxResult add(@RequestBody CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogAddwxService.insertCompanyVoiceRoboticCallLogAddwx(companyVoiceRoboticCallLogAddwx));
+//    }
+//
+//    /**
+//     * 修改调用日志_加微信
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:addwxlog:edit')")
+//    @Log(title = "调用日志_加微信", businessType = BusinessType.UPDATE)
+//    @PutMapping
+//    public AjaxResult edit(@RequestBody CompanyVoiceRoboticCallLogAddwx companyVoiceRoboticCallLogAddwx)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogAddwxService.updateCompanyVoiceRoboticCallLogAddwx(companyVoiceRoboticCallLogAddwx));
+//    }
+//
+//    /**
+//     * 删除调用日志_加微信
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:addwxlog:remove')")
+//    @Log(title = "调用日志_加微信", businessType = BusinessType.DELETE)
+//	@DeleteMapping("/{logIds}")
+//    public AjaxResult remove(@PathVariable Long[] logIds)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogAddwxService.deleteCompanyVoiceRoboticCallLogAddwxByLogIds(logIds));
+//    }
+}

+ 162 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java

@@ -0,0 +1,162 @@
+package com.fs.company.controller.company;
+
+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.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 调用日志_ai打电话Controller
+ * 
+ * @author fs
+ * @date 2026-01-15
+ */
+@RestController
+@RequestMapping("/company/callphoneLog")
+public class CompanyVoiceRoboticCallLogCallphoneController extends BaseController
+{
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
+
+    /**
+     * 查询调用日志_ai打电话列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphonelog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        if (null == companyVoiceRoboticCallLogCallphone.getRoboticId() && null == companyVoiceRoboticCallLogCallphone.getCallerId()) {
+            if (companyVoiceRoboticCallLogCallphone.getCustomerId() != null) {
+                List<Long> calleeIds = companyVoiceRoboticCallLogCallphoneService.getCallerIdsByCustomerId(companyVoiceRoboticCallLogCallphone.getCustomerId());
+                if(null == calleeIds || calleeIds.isEmpty()){
+                    return getDataTable(new ArrayList<>());
+                }
+                companyVoiceRoboticCallLogCallphone.setCallerIds(calleeIds);
+                startPage();
+                List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallLogCallphoneListData(companyVoiceRoboticCallLogCallphone);
+                return getDataTable(list);
+            } else {
+                return getDataTable(new ArrayList<>());
+            }
+        } else {
+            startPage();
+            List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallLogCallphoneListData(companyVoiceRoboticCallLogCallphone);
+            return getDataTable(list);
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/listByCallerIdAndRoboticId")
+    public TableDataInfo listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = companyVoiceRoboticCallLogCallphoneService.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+        return getDataTable(list);
+
+    }
+
+
+    /**
+     * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/groupList")
+    public TableDataInfo groupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询调用日志_发送短信列表统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/count")
+    public AjaxResult selectCompanyVoiceRoboticCallPhoneLogCount()
+    {
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogCount();
+        return AjaxResult.success(companyVoiceRoboticCallLogCount);
+    }
+
+    /**
+     * 导出调用日志_ai打电话列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphonelog:export')")
+    @Log(title = "调用日志_ai打电话", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = companyVoiceRoboticCallLogCallphoneService.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+        ExcelUtil<CompanyVoiceRoboticCallLogCallPhoneVO> util = new ExcelUtil<CompanyVoiceRoboticCallLogCallPhoneVO>(CompanyVoiceRoboticCallLogCallPhoneVO.class);
+        return util.exportExcel(list, "调用日志_ai打电话数据");
+    }
+
+//    /**
+//     * 导出调用日志_ai打电话列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:callphonelog:export')")
+//    @Log(title = "调用日志_ai打电话", businessType = BusinessType.EXPORT)
+//    @GetMapping("/export")
+//    public AjaxResult export(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+//    {
+//        List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallLogCallphoneList(companyVoiceRoboticCallLogCallphone);
+//        ExcelUtil<CompanyVoiceRoboticCallLogCallphone> util = new ExcelUtil<CompanyVoiceRoboticCallLogCallphone>(CompanyVoiceRoboticCallLogCallphone.class);
+//        return util.exportExcel(list, "调用日志_ai打电话数据");
+//    }
+//
+//    /**
+//     * 获取调用日志_ai打电话详细信息
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:callphonelog:query')")
+//    @GetMapping(value = "/{logId}")
+//    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+//    {
+//        return AjaxResult.success(companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallLogCallphoneByLogId(logId));
+//    }
+//
+//    /**
+//     * 新增调用日志_ai打电话
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:callphonelog:add')")
+//    @Log(title = "调用日志_ai打电话", businessType = BusinessType.INSERT)
+//    @PostMapping
+//    public AjaxResult add(@RequestBody CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogCallphoneService.insertCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLogCallphone));
+//    }
+//
+//    /**
+//     * 修改调用日志_ai打电话
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:callphonelog:edit')")
+//    @Log(title = "调用日志_ai打电话", businessType = BusinessType.UPDATE)
+//    @PutMapping
+//    public AjaxResult edit(@RequestBody CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogCallphoneService.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLogCallphone));
+//    }
+//
+//    /**
+//     * 删除调用日志_ai打电话
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:callphonelog:remove')")
+//    @Log(title = "调用日志_ai打电话", businessType = BusinessType.DELETE)
+//	@DeleteMapping("/{logIds}")
+//    public AjaxResult remove(@PathVariable Long[] logIds)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogCallphoneService.deleteCompanyVoiceRoboticCallLogCallphoneByLogIds(logIds));
+//    }
+}

+ 142 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java

@@ -0,0 +1,142 @@
+package com.fs.company.controller.company;
+
+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.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogSendmsgService;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 调用日志_发送短信Controller
+ * 
+ * @author fs
+ * @date 2026-01-15
+ */
+@RestController
+@RequestMapping("/company/sendmsgLog")
+public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
+{
+    @Autowired
+    private ICompanyVoiceRoboticCallLogSendmsgService companyVoiceRoboticCallLogSendmsgService;
+
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
+    /**
+     * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgGroupList(companyVoiceRoboticCallLogSendmsg);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询调用日志_发送短信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/listByCallerIdAndRoboticId")
+    public TableDataInfo listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
+    {
+        if (null == companyVoiceRoboticCallLogSendmsg.getRoboticId() && null == companyVoiceRoboticCallLogSendmsg.getCallerId()) {
+            if (companyVoiceRoboticCallLogSendmsg.getCustomerId() != null) {
+                List<Long> calleeIds = companyVoiceRoboticCallLogCallphoneService.getCallerIdsByCustomerId(companyVoiceRoboticCallLogSendmsg.getCustomerId());
+                if(null == calleeIds || calleeIds.isEmpty()){
+                    return getDataTable(new ArrayList<>());
+                }
+                companyVoiceRoboticCallLogSendmsg.setCallerIds(calleeIds);
+                List<CompanyVoiceRoboticCallLogSendmsgVO> list = companyVoiceRoboticCallLogSendmsgService.listByCallerIdAndRoboticId(companyVoiceRoboticCallLogSendmsg);
+                return getDataTable(list);
+            } else{
+                return getDataTable(new ArrayList<>());
+            }
+        }else{
+            startPage();
+            List<CompanyVoiceRoboticCallLogSendmsgVO> list = companyVoiceRoboticCallLogSendmsgService.listByCallerIdAndRoboticId(companyVoiceRoboticCallLogSendmsg);
+            return getDataTable(list);
+        }
+    }
+
+    /**
+     * 查询调用日志_发送短信列表统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/count")
+    public AjaxResult selectCompanyVoiceRoboticCallLogSendMsgCount()
+    {
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendMsgCount();
+        return AjaxResult.success(companyVoiceRoboticCallLogCount);
+    }
+
+    /**
+     * 导出调用日志_发送短信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:export')")
+    @Log(title = "调用日志_发送短信", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
+    {
+        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgList(companyVoiceRoboticCallLogSendmsg);
+        ExcelUtil<CompanyVoiceRoboticCallLogSendmsg> util = new ExcelUtil<CompanyVoiceRoboticCallLogSendmsg>(CompanyVoiceRoboticCallLogSendmsg.class);
+        return util.exportExcel(list, "调用日志_发送短信数据");
+    }
+//
+//    /**
+//     * 获取调用日志_发送短信详细信息
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:sendmsglog:query')")
+//    @GetMapping(value = "/{logId}")
+//    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+//    {
+//        return AjaxResult.success(companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgByLogId(logId));
+//    }
+//
+//    /**
+//     * 新增调用日志_发送短信
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:sendmsglog:add')")
+//    @Log(title = "调用日志_发送短信", businessType = BusinessType.INSERT)
+//    @PostMapping
+//    public AjaxResult add(@RequestBody CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogSendmsgService.insertCompanyVoiceRoboticCallLogSendmsg(companyVoiceRoboticCallLogSendmsg));
+//    }
+//
+//    /**
+//     * 修改调用日志_发送短信
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:sendmsglog:edit')")
+//    @Log(title = "调用日志_发送短信", businessType = BusinessType.UPDATE)
+//    @PutMapping
+//    public AjaxResult edit(@RequestBody CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogSendmsgService.updateCompanyVoiceRoboticCallLogSendmsg(companyVoiceRoboticCallLogSendmsg));
+//    }
+
+//    /**
+//     * 删除调用日志_发送短信
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:sendmsglog:remove')")
+//    @Log(title = "调用日志_发送短信", businessType = BusinessType.DELETE)
+//	@DeleteMapping("/{logIds}")
+//    public AjaxResult remove(@PathVariable Long[] logIds)
+//    {
+//        return toAjax(companyVoiceRoboticCallLogSendmsgService.deleteCompanyVoiceRoboticCallLogSendmsgByLogIds(logIds));
+//    }
+}

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

@@ -1,7 +1,10 @@
 package com.fs.company.controller.company;
 
+import com.alibaba.fastjson.JSONObject;
+import com.fs.aicall.domain.BaseDomain;
 import com.fs.aicall.domain.TaskInfo;
 import com.fs.aicall.domain.apiresult.PushIIntentionResult;
+import com.fs.aicall.domain.result.CIDGroupListResult;
 import com.fs.aicall.domain.result.EditDialogResult;
 import com.fs.aicall.domain.result.GetairobotResult;
 import com.fs.aicall.domain.result.QueryCallTaskInfoResult;
@@ -21,8 +24,10 @@ import com.fs.company.domain.CompanyVoiceRoboticWx;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
+import com.fs.company.vo.CdrDetailVo;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
@@ -40,6 +45,7 @@ import java.util.stream.Collectors;
  */
 @RestController
 @RequestMapping("/company/companyVoiceRobotic")
+@Slf4j
 public class CompanyVoiceRoboticController extends BaseController
 {
     @Autowired
@@ -59,6 +65,8 @@ public class CompanyVoiceRoboticController extends BaseController
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:list')")
     @GetMapping("/list")
     public TableDataInfo list(CompanyVoiceRobotic companyVoiceRobotic){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
         return getDataTable(list);
@@ -69,6 +77,8 @@ public class CompanyVoiceRoboticController extends BaseController
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:list')")
     @GetMapping("/listAll")
     public R listAll(CompanyVoiceRobotic companyVoiceRobotic){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         return R.ok().put("data", companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic));
     }
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:list')")
@@ -87,6 +97,14 @@ public class CompanyVoiceRoboticController extends BaseController
         return getDataTable(list);
     }
 
+    //    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:wxListQw')")
+    @GetMapping("/wxListQw")
+    public TableDataInfo wxListQw(Long id){
+        startPage();
+        List<CompanyVoiceRoboticWx> list = companyVoiceRoboticWxService.selectWxListQw(id);
+        return getDataTable(list);
+    }
+
     /**
      * 导出机器人外呼任务列表
      */
@@ -95,6 +113,8 @@ public class CompanyVoiceRoboticController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(CompanyVoiceRobotic companyVoiceRobotic)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
         ExcelUtil<CompanyVoiceRobotic> util = new ExcelUtil<CompanyVoiceRobotic>(CompanyVoiceRobotic.class);
         return util.exportExcel(list, "robotic");
@@ -120,6 +140,8 @@ public class CompanyVoiceRoboticController extends BaseController
     public AjaxResult add(@RequestBody CompanyVoiceRobotic companyVoiceRobotic){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         companyVoiceRobotic.setCreateUser(loginUser.getUser().getUserId());
+        companyVoiceRobotic.setCompanyUserId(loginUser.getUser().getUserId());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         return toAjax(companyVoiceRoboticService.insertCompanyVoiceRobotic(companyVoiceRobotic));
     }
 
@@ -140,18 +162,23 @@ public class CompanyVoiceRoboticController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:remove')")
     @Log(title = "机器人外呼任务", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{ids}")
+    @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
     {
-        return toAjax(companyVoiceRoboticService.deleteCompanyVoiceRoboticByIds(ids));
+        for (Long id : ids) {
+            companyVoiceRoboticService.updateDelFlag(id, 1);
+        }
+        return AjaxResult.success();
     }
     /**
      * 删除机器人外呼任务
      */
-	@GetMapping("/getTypes")
+    @GetMapping("/getTypes")
     public R getTypes(){
-        List<GetairobotResult> getairobotlist = aiCallService.getairobotlist();
-        List<EditDialogResult> editDialogResults = aiCallService.queryDialog();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        List<GetairobotResult> getairobotlist = aiCallService.getairobotlist(companyId);
+        List<EditDialogResult> editDialogResults = aiCallService.queryDialog(companyId);
         return R.ok().put("robot", getairobotlist).put("dialog", editDialogResults);
     }
     /**
@@ -159,29 +186,35 @@ public class CompanyVoiceRoboticController extends BaseController
      */
     @GetMapping("/statusList")
     public R statusList(String ids){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
         List<String> list = Arrays.stream(ids.split(",")).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
-        return R.ok().put("data", list.stream().map(e -> aiCallService.queryCallTaskInfo(TaskInfo.builder().taskID(e).build())).collect(Collectors.toMap(QueryCallTaskInfoResult::getTaskID, e -> e)));
+        return R.ok().put("data", list.stream().map(e -> aiCallService.queryCallTaskInfo(TaskInfo.builder().taskID(e).build(), companyId)).collect(Collectors.toMap(QueryCallTaskInfoResult::getTaskID, e -> e)));
     }
     /**
      * 启动任务
      */
-	@GetMapping("/startRobotic")
+    @GetMapping("/startRobotic")
     public R startRobotic(String taskId){
-        aiCallService.startCallTask(TaskInfo.builder().taskID(taskId).build());
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        aiCallService.startCallTask(TaskInfo.builder().taskID(taskId).build(), companyId);
         return R.ok();
     }
     /**
      * 停止任务
      */
-	@GetMapping("/stopRobotic")
+    @GetMapping("/stopRobotic")
     public R stopRobotic(String taskId){
-        aiCallService.stopCallTask(TaskInfo.builder().taskID(taskId).build());
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        aiCallService.stopCallTask(TaskInfo.builder().taskID(taskId).build(), companyId);
         return R.ok();
     }
     /**
      * 员工列表
      */
-	@GetMapping("/companyUserList")
+    @GetMapping("/companyUserList")
     public R qwUserList(){
         return R.ok().put("data", companyVoiceRoboticService.qwUserListCompany(new CompanyVoiceRobotic()));
     }
@@ -189,18 +222,83 @@ public class CompanyVoiceRoboticController extends BaseController
     /**
      * 外呼回调
      */
-	@PostMapping("/callerResult")
+    @PostMapping("/callerResult")
     public R callerResult(@RequestBody PushIIntentionResult result){
         companyVoiceRoboticService.callerResult(result);
         return R.ok();
     }
+
+    @PostMapping("/callerResult4EasyCall")
+    public String callerResult4EasyCall(@RequestBody String cdrStr) {
+        log.info("callerResult4EasyCall:回调结果:{}",cdrStr);
+        CdrDetailVo cdrDetailVo = JSONObject.parseObject(cdrStr, CdrDetailVo.class);
+        companyVoiceRoboticService.callerResult4EasyCall(cdrDetailVo);
+        return "success";
+    }
     /**
      * 外呼回调
      */
-	@GetMapping("/test")
+    @GetMapping("/test")
     public R test(Long id){
-//        companyVoiceRoboticService.test(id);
         companyVoiceRoboticService.dispenseWx(id);
         return R.ok();
     }
+    /**
+     * 获取主叫
+     */
+    @GetMapping("/cidList")
+    public R cidList(){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        companyVoiceRoboticService.cidList(companyId);
+        return R.ok();
+    }
+    /**
+     * 启动任务
+     */
+    @GetMapping("/taskRun")
+    public R taskRun(Long id){
+        companyVoiceRoboticService.taskRun(id);
+        return R.ok();
+    }
+
+    @GetMapping("/getCIDGroupList")
+    public R getCIDGroupList(){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CIDGroupListResult> cidGroupList = aiCallService.getCIDGroupList(null, loginUser.getCompany().getCompanyId());
+        return R.ok().put("data", cidGroupList);
+    }
+
+    /**
+     * 查询任务执行记录
+     * 获取每个人的工作流执行状态和节点日志
+     */
+//    @GetMapping("/execRecords")
+//    public R getExecRecords(Long roboticId) {
+//        if (roboticId == null) {
+//            return R.error("任务ID不能为空");
+//        }
+//        List<WorkflowExecRecordVo> records = companyVoiceRoboticService.getExecRecords(roboticId);
+//        return R.ok().put("data", records);
+//    }
+
+    @GetMapping("/execRecords")
+    public R getExecRecords(@RequestParam Long roboticId,
+                            @RequestParam(defaultValue = "1") Integer pageNum,
+                            @RequestParam(defaultValue = "10") Integer pageSize,
+                            @RequestParam(required = false) String customerName,
+                            @RequestParam(required = false) String customerPhone,
+                            @RequestParam Boolean onlyCallNode) {
+        if (roboticId == null) {
+            return R.error("任务ID不能为空");
+        }
+        return R.ok(companyVoiceRoboticService.getExecRecords(roboticId, pageNum, pageSize, customerName,
+                customerPhone,onlyCallNode));
+    }
+
+    @GetMapping("/getCurrentCompanyId")
+    public R getCurrentCompanyId(){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return R.ok().put("companyId", loginUser.getCompany().getCompanyId());
+    }
 }

+ 181 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java

@@ -0,0 +1,181 @@
+package com.fs.company.controller.company;
+
+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.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyWorkflow;
+import com.fs.company.domain.CompanyWorkflowNodeType;
+import com.fs.company.param.CompanyWorkflowSaveParam;
+import com.fs.company.param.CompanyWorkflowUpdateBindWCParam;
+import com.fs.company.service.ICompanyWorkflowService;
+import com.fs.company.vo.OptionVO;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * AI工作流Controller
+ *
+ * @author fs
+ * @date 2026-01-06
+ */
+@RestController
+@RequestMapping("/company/companyWorkflow")
+public class CompanyWorkflowController extends BaseController {
+
+    @Autowired
+    private ICompanyWorkflowService companyWorkflowService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询AI工作流列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyWorkflow fsAiWorkflow) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsAiWorkflow.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<CompanyWorkflow> list = companyWorkflowService.selectCompanyWorkflowList(fsAiWorkflow);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出AI工作流列表
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyWorkflow fsAiWorkflow) {
+        List<CompanyWorkflow> list = companyWorkflowService.selectCompanyWorkflowList(fsAiWorkflow);
+        ExcelUtil<CompanyWorkflow> util = new ExcelUtil<CompanyWorkflow>(CompanyWorkflow.class);
+        return util.exportExcel(list, "AI工作流数据");
+    }
+
+    /**
+     * 获取AI工作流详细信息(包含节点和连线)
+     */
+    @GetMapping(value = "/{workflowId}")
+    public AjaxResult getInfo(@PathVariable("workflowId") Long workflowId) {
+        return AjaxResult.success(companyWorkflowService.selectCompanyWorkflowById(workflowId));
+    }
+
+    /**
+     * 保存AI工作流(新增或更新)
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.INSERT)
+    @PostMapping("/save")
+    public AjaxResult save(@RequestBody CompanyWorkflowSaveParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getUser().getCompanyId());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        Long workflowId = companyWorkflowService.saveCompanyWorkflow(param);
+        return AjaxResult.success(workflowId);
+    }
+
+    /**
+     * 修改AI工作流状态
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.UPDATE)
+    @PutMapping("/status/{workflowId}/{status}")
+    public AjaxResult updateStatus(@PathVariable("workflowId") Long workflowId,
+                                   @PathVariable("status") Integer status) {
+        return toAjax(companyWorkflowService.updateCompanyWorkflowStatus(workflowId, status));
+    }
+
+    /**
+     * 删除AI工作流
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{workflowIds}")
+    public AjaxResult remove(@PathVariable Long[] workflowIds) {
+        return toAjax(companyWorkflowService.deleteCompanyWorkflowByIds(workflowIds));
+    }
+
+    /**
+     * 复制AI工作流
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.INSERT)
+    @PostMapping("/copy/{workflowId}")
+    public AjaxResult copy(@PathVariable("workflowId") Long workflowId) {
+        Long newWorkflowId = companyWorkflowService.copyCompanyWorkflow(workflowId);
+        if (newWorkflowId != null) {
+            return AjaxResult.success(newWorkflowId);
+        }
+        return AjaxResult.error("复制失败,工作流不存在");
+    }
+
+    /**
+     * 获取所有启用的节点类型
+     */
+    @GetMapping("/nodeTypes")
+    public AjaxResult getNodeTypes() {
+        List<CompanyWorkflowNodeType> list = companyWorkflowService.selectAllEnabledNodeTypes();
+        return AjaxResult.success(list);
+    }
+    /**
+     * 导出工作流流程图JSON
+     * 包含节点信息、连接顺序、节点类型
+     */
+//    @Log(title = "AI工作流", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportJson/{workflowId}")
+    public AjaxResult exportJson(@PathVariable("workflowId") Long workflowId) {
+        return AjaxResult.success(companyWorkflowService.exportWorkflowJson(workflowId));
+    }
+    /**
+     * 分页销售
+     */
+    @GetMapping("/listCompanyUser")
+    public AjaxResult listCompanyUser() {
+        return AjaxResult.success(companyWorkflowService.listCompanyUser());
+    }
+
+    /**
+     * 查销售
+     */
+    @GetMapping("/getCompanyUserById/{companyUserId}")
+    public AjaxResult getCompanyUserById(@PathVariable("companyUserId") Long companyUserId) {
+        return AjaxResult.success(companyWorkflowService.getCompanyUserById(companyUserId));
+    }
+
+//    /**
+//     * 查销售是否已经被绑定
+//     */
+//    @GetMapping("/checkCompanyUserBeUsed/{companyUserId}")
+//    public AjaxResult checkCompanyUserBeUsed(@PathVariable("companyUserId") Long companyUserId) {
+//        return AjaxResult.success(companyWorkflowService.checkCompanyUserBeUsed(companyUserId));
+//    }
+
+    /**
+     * 查工作流已绑定的销售
+     */
+    @GetMapping("/getBindCompanyUserByWorkflowId/{workflowId}")
+    public AjaxResult getBindCompanyUserByWorkflowId(@PathVariable("workflowId") Long workflowId) {
+        return AjaxResult.success(companyWorkflowService.getBindCompanyUserByWorkflowId(workflowId));
+    }
+
+    /**
+     * 修改工作流绑定的销售
+     */
+    @PostMapping("/updateWorkflowBindCompanyUser")
+    public AjaxResult updateWorkflowBindCompanyUser(@RequestBody CompanyWorkflowUpdateBindWCParam param) {
+        return companyWorkflowService.updateWorkflowBindCompanyUser(param);
+    }
+    /**
+     * 修改工作流绑定的销售
+     */
+    @GetMapping("/optionList")
+    public R optionList() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<OptionVO> optionVOS = companyWorkflowService.optionList(loginUser.getCompany().getCompanyId());
+        return R.ok().put("data",optionVOS);
+    }
+
+}

+ 20 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -113,6 +113,26 @@ public class CrmCustomerController extends BaseController
         return R.ok().put("rows", list);
     }
 
+    @PreAuthorize("@ss.hasPermi('crm:customer:list')")
+    @GetMapping("/listNoPage")
+    public R listNoPage(CrmCustomerListQueryParam crmCustomer){
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+        if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
+            crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));
+        }
+        List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListQueryParam(crmCustomer);
+        if (list != null) {
+            for (CrmCustomerListVO vo : list) {
+                if(vo.getMobile()!=null){
+                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+            }
+        }
+        return R.ok().put("rows", list);
+    }
+
     @ApiOperation("获取我的协作客户列表")
     @PreAuthorize("@ss.hasPermi('crm:customer:assistList')")
     @GetMapping("/getMyAssistList")

+ 170 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java

@@ -0,0 +1,170 @@
+package com.fs.company.controller.crm;
+
+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.domain.R;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.domain.CrmCustomerProperty;
+import com.fs.crm.service.ICrmCustomerPropertyService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@Api(tags = "客户属性标签管理")
+@RestController
+@RequestMapping("/crm/customerProperty")
+public class CrmCustomerPropertyController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @ApiOperation("根据客户 ID 查询属性标签列表")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/list/{customerId}")
+    public R listByCustomerId(@PathVariable("customerId") Long customerId) {
+        List<CrmCustomerProperty> list = crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(customerId);
+        return R.ok().put("data", list);
+    }
+
+    @ApiOperation("查询单个属性标签详情")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/{id}")
+    public R getInfo(@PathVariable("id") Long id) {
+        CrmCustomerProperty property = crmCustomerPropertyService.selectCrmCustomerPropertyById(id);
+        return R.ok().put("data", property);
+    }
+
+    @ApiOperation("为客户添加属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        property.setCreateBy(loginUser.getUsername());
+        return toAjax(crmCustomerPropertyService.insertCrmCustomerProperty(property));
+    }
+
+    @ApiOperation("为客户添加或更新属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/addOrUpdate")
+    public AjaxResult addOrUpdate(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addOrUpdateCustomerPropertyWithExtra(
+                property.getCustomerId(),
+                property.getPropertyId(),
+                property.getPropertyName(),
+                property.getPropertyValue(),
+                property.getPropertyValueType(),
+                property.getTradeType(),
+                property.getIntention(),
+                property.getLikeRatio(),
+                loginUser.getUsername()
+        ));
+    }
+
+    @ApiOperation("批量为客户添加属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/batchAdd/{customerId}")
+    public AjaxResult batchAdd(
+            @PathVariable("customerId") Long customerId,
+            @RequestBody List<CrmCustomerProperty> properties) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        for (CrmCustomerProperty property : properties) {
+            property.setCreateBy(loginUser.getUsername());
+        }
+        return toAjax(crmCustomerPropertyService.batchAddCustomerProperties(customerId, properties));
+    }
+
+    @ApiOperation("通过属性模板 ID 为客户添加标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/addByTemplateId")
+    public AjaxResult addByTemplateId(
+            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+            @ApiParam(required = true, name = "templateId", value = "属性模板 ID") @RequestParam Long templateId,
+            @ApiParam(required = true, name = "propertyValue", value = "属性值") @RequestParam String propertyValue) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addPropertyByTemplateId(customerId, templateId, propertyValue, loginUser.getUsername()));
+    }
+
+    @ApiOperation("通过属性模板 ID 为客户添加或更新标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/addOrUpdateByTemplateId")
+    public AjaxResult addOrUpdateByTemplateId(
+            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+            @ApiParam(required = true, name = "templateId", value = "属性模板 ID") @RequestParam Long templateId,
+            @ApiParam(required = true, name = "propertyValue", value = "属性值") @RequestParam String propertyValue) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addOrUpdatePropertyByTemplateId(customerId, templateId, propertyValue, loginUser.getUsername()));
+    }
+
+    @ApiOperation("批量通过属性模板 ID 为客户添加标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/batchAddByTemplateIds/{customerId}")
+    public AjaxResult batchAddByTemplateIds(
+            @PathVariable("customerId") Long customerId,
+            @RequestBody Map<Long, String> propertyMap) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.batchAddPropertiesByTemplateIds(customerId, propertyMap, loginUser.getUsername()));
+    }
+
+    @ApiOperation("修改客户属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        property.setUpdateBy(loginUser.getUsername());
+        return toAjax(crmCustomerPropertyService.updateCrmCustomerProperty(property));
+    }
+
+    @ApiOperation("删除客户属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(crmCustomerPropertyService.deleteCrmCustomerPropertyByIds(ids));
+    }
+
+    @ApiOperation("删除客户单个属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+    @DeleteMapping("/deleteByPropertyId")
+    public AjaxResult deleteByPropertyId(
+            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+            @ApiParam(required = true, name = "propertyId", value = "属性模板 ID") @RequestParam Long propertyId) {
+        return toAjax(crmCustomerPropertyService.lambdaUpdate()
+                .eq(CrmCustomerProperty::getCustomerId, customerId)
+                .eq(CrmCustomerProperty::getPropertyId, propertyId)
+                .remove());
+    }
+
+    @ApiOperation("导出客户属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:export')")
+    @Log(title = "客户属性标签", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerProperty crmCustomerProperty) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CrmCustomerProperty> list = crmCustomerPropertyService.selectCrmCustomerPropertyList(crmCustomerProperty);
+        ExcelUtil<CrmCustomerProperty> util = new ExcelUtil<CrmCustomerProperty>(CrmCustomerProperty.class);
+        return util.exportExcel(list, "客户属性标签数据");
+    }
+}

+ 78 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyTemplateController.java

@@ -0,0 +1,78 @@
+package com.fs.company.controller.crm;
+
+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.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Api(tags = "客户属性标签模板管理")
+@RestController
+@RequestMapping("/crm/customerPropertyTemplate")
+public class CrmCustomerPropertyTemplateController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerPropertyTemplateService crmCustomerPropertyTemplateService;
+
+    @ApiOperation("查询客户属性标签模板列表")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        startPage();
+        List<CrmCustomerPropertyTemplate> list = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("查询客户属性标签模板详情")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/{id}")
+    public R getInfo(@PathVariable("id") Long id) {
+        CrmCustomerPropertyTemplate template = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateById(id);
+        return R.ok().put("data", template);
+    }
+
+    @ApiOperation("添加客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return toAjax(crmCustomerPropertyTemplateService.insertCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate));
+    }
+
+    @ApiOperation("修改客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return toAjax(crmCustomerPropertyTemplateService.updateCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate));
+    }
+
+    @ApiOperation("删除客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(crmCustomerPropertyTemplateService.deleteCrmCustomerPropertyTemplateByIds(ids));
+    }
+
+    @ApiOperation("导出客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:export')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        List<CrmCustomerPropertyTemplate> list = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+        ExcelUtil<CrmCustomerPropertyTemplate> util = new ExcelUtil<CrmCustomerPropertyTemplate>(CrmCustomerPropertyTemplate.class);
+        return util.exportExcel(list, "客户属性标签模板数据");
+    }
+}

+ 14 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java

@@ -497,7 +497,20 @@ public class QwUserController extends BaseController
         List<QwUser> list = qwUserService.selectQwUserList(qwUser);
         return getDataTable(list);
     }
-
+    /**
+     * 查询企微用户列表-不固定销售公司查询
+     */
+    @GetMapping("/queryQwList")
+    public TableDataInfo queryQwList(QwUser qwUser)
+    {
+        startPage();
+        if(qwUser.getCompanyId() == null){
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        }
+        List<QwUser> list = qwUserService.selectQwUserList(qwUser);
+        return getDataTable(list);
+    }
     /**
     * 查询企微员工列表-用于员工管理绑定
     */

+ 9 - 1
fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java

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

+ 134 - 0
fs-service/src/main/java/com/fs/aiSipCall/RemoteCommon.java

@@ -0,0 +1,134 @@
+package com.fs.aiSipCall;
+
+import cn.hutool.http.HttpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * @Author:peicj
+ * @Description: 远程接口公共参数
+ * @Date:2026/3/9 10:50
+ */
+@Slf4j
+@Component
+public class RemoteCommon {
+    /**
+     * 远程接口地址前缀
+     */
+//    public static final String REMOTE_ADDERSS_PREFIX = "http://129.28.164.235:8899";
+    public static final String REMOTE_ADDERSS_PREFIX = "http://127.0.0.1:8899";
+
+    /**
+     * 网关列表接口
+     */
+    public static final String GATEWAY_LIST_API = "/aicall/api/gateway/list";
+    /**
+     * 音色列表接口
+     */
+    public static final String VOICECODE_LIST_API = "/aicall/api/voicecode/list";
+    /**
+     * 大模型列表接口
+     */
+    public static final String LLMACOUNT_LIST_API = "/aicall/api/llmacount/list";
+    /**
+     * 技能组列表接口
+     */
+    public static final String BUSIGROUP_LIST_API = "/aicall/api/busigroup/list";
+    /**
+     * 查询AI外呼通话记录列表接口
+     */
+    public static final String CALL_RECORDS_API = "/aicall/api/call/phone/records";
+    /**
+     * 新增任务接口
+     */
+    public static final String CREATE_TASK_API = "/aicall/api/ai/createTask";
+    /**
+     * 修改任务接口
+     */
+    public static final String EDIT_TASK_API = "/aicall/api/createTask";
+    /**
+     * 启动任务接口
+     */
+    public static final String START_TASK_API = "/aicall/api/ai/startTask";
+    /**
+     * 暂停任务接口
+     */
+    public static final String STOP_TASK_API = "/aicall/api/ai/stopTask";
+    /**
+     * AI外呼追加名单接口
+     */
+    public static final String ADD_CALL_LIST_API = "/aicall/api/ai/addCallList";
+    /**
+     * 通用追加名单接口
+     */
+    public static final String COMMON_ADD_CALL_LIST_API = "/aicall/api/common/addCallList";
+    /**
+     * 录音文件下载接口
+     */
+    public static final String RECORDINGS_FILES_API = "/recordings/files";
+    /**
+     * 获取电话工具条的网关列表
+     */
+    public static final String PHONEBAR_PARAMS_API = "/aicall/api/phoneBar/params";
+
+    /**
+     * 新增用户并绑定分机
+     */
+    public static final String ADD_USER_OR_BIND_EXTNUM_API = "/aicall/api/user/addUserOrBindExtNumReturnUser";
+    /**
+     * 修改用户
+     */
+    public static final String EDIT_USER_OR_UNBING_EXTNUM_API = "/aicall/api/user/editUserOrUnBindExtNum";
+
+
+    /**
+     * 查询未分配的分机
+     */
+    public static final String QUERY_UN_BIND_EXTNUM_API = "/aicall/api/extnum/selectUnBindCcExtNumList";
+
+    /**
+     * 获取手动外呼客户沟通信息
+     */
+    public static final String GET_CUST_COMMUNICATION_INFO_API = "/aicall/api/getCustCommunicationInfo";
+    /**
+     * 新增保存手动外呼沟通记录
+     */
+    public static final String ADD_CUSTCALL_RECORD_API = "/aicall/api/add/custcallrecord";
+    /**
+     * 查询外呼记录列表
+     */
+    public static final String QUERY_OUTBOUNDCDR_LIST_API = "/aicall/api/outboundcdrList";
+    /**
+     * 根据uuid和外呼类型查询外呼记录
+     */
+    public static final String QUERY_OUTBOUNDCDR_BYUUID_API = "/aicall/api/record/uuid";
+
+    /**
+     * 发送get请求
+     * @param url   地址
+     * @return  String  结果
+     */
+    public static String sendGet(String url){
+        try{
+            return HttpUtil.get(url, 10 * 1000);
+        }catch (Exception e){
+            e.printStackTrace();
+            log.info("sendGet error");
+        }
+        return null;
+    }
+    /**
+     * 发送post请求
+     * @param url   地址
+     * @return  String  结果
+     */
+    public static String sendPost(String url,String jsonBody){
+        try{
+            return HttpUtil.post(url, jsonBody);
+        }catch (Exception e){
+            e.printStackTrace();
+            log.info("sendPost error");
+        }
+        return null;
+    }
+}

+ 38 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallBizGroup.java

@@ -0,0 +1,38 @@
+package com.fs.aiSipCall.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * aiSIP外呼技能组对象 ai_sip_call_biz_group
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiSipCallBizGroup extends BaseEntity{
+
+    /** 业务组编号 */
+    private Long groupId;
+
+    /** 业务组名称 */
+    @Excel(name = "业务组名称")
+    private String bizGroupName;
+
+    /** 排序 */
+    @Excel(name = "排序")
+    private Long sortNo;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String notes;
+
+    /** 远程技能组ID */
+    @Excel(name = "远程技能组ID")
+    private Long remoteGroupId;
+
+
+}

+ 85 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallGateway.java

@@ -0,0 +1,85 @@
+package com.fs.aiSipCall.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * aiSIP外呼网关对象 ai_sip_call_gateway
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiSipCallGateway extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 网关名称 */
+    @Excel(name = "网关名称")
+    private String gwName;
+
+    /** profile名称 */
+    @Excel(name = "profile名称")
+    private String profileName;
+
+    /** 外呼的主叫号码 */
+    @Excel(name = "外呼的主叫号码")
+    private String caller;
+
+    /** 被叫前缀 */
+    @Excel(name = "被叫前缀")
+    private String calleePrefix;
+
+    /** 网关地址; ip地址:端口 */
+    @Excel(name = "网关地址; ip地址:端口")
+    private String gwAddr;
+
+    /** 外呼语音编码 */
+    @Excel(name = "外呼语音编码")
+    private String codec;
+
+    /** 网关名称描述; */
+    @Excel(name = "网关名称描述;")
+    private String gwDesc;
+
+    /** 网关最大并发数 */
+    @Excel(name = "网关最大并发数")
+    private Long maxConcurrency;
+
+    /** 注册模式下;认证用户名 */
+    @Excel(name = "注册模式下;认证用户名")
+    private String authUsername;
+
+    /** 注册模式下;认证密码 */
+    @Excel(name = "注册模式下;认证密码")
+    private String authPassword;
+
+    /** 使用优先级; 数字1-9; 数字越小,越优先被使用 */
+    @Excel(name = "使用优先级; 数字1-9; 数字越小,越优先被使用")
+    private Long priority;
+
+    /** 是否需要认证注册; 0 对接模式; 1注册模式 */
+    @Excel(name = "是否需要认证注册; 0 对接模式; 1注册模式")
+    private Long register;
+
+    /** 自定义参数 */
+    @Excel(name = "自定义参数")
+    private String configs;
+
+    /** 0 已废弃; 1 电话条; 2 外呼任务; 3 无限制 */
+    @Excel(name = "0 已废弃; 1 电话条; 2 外呼任务; 3 无限制")
+    private Long purpose;
+
+    /** 远程网关ID */
+    @Excel(name = "远程网关ID")
+    private Long remoteGatewayId;
+
+    private Long pageSize;
+
+    private Long pageNum;
+
+}

+ 70 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallLlmAgentAccount.java

@@ -0,0 +1,70 @@
+package com.fs.aiSipCall.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * aiSIP外呼大模型对象 ai_sip_call_llm_agent_account
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiSipCallLlmAgentAccount extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 账号详细信息 */
+    @Excel(name = "账号详细信息")
+    private String accountJson;
+
+    /** 实现类,枚举值:DeepSeekChat, Coze, MaxKB, Dify */
+    @Excel(name = "实现类,枚举值:DeepSeekChat, Coze, MaxKB, Dify")
+    private String providerClassName;
+
+    /** 别名 */
+    @Excel(name = "别名")
+    private String name;
+
+    /** 大模型类型,枚举值:LlmAccount, CozeAccount */
+    @Excel(name = "大模型类型,枚举值:LlmAccount, CozeAccount")
+    private String accountEntity;
+
+    /** 0:不打断,1:关键词打断,2:有声音就打断 */
+    @Excel(name = "0:不打断,1:关键词打断,2:有声音就打断")
+    private Long interruptFlag;
+
+    /** 打断关键词列表 */
+    @Excel(name = "打断关键词列表")
+    private String interruptKeywords;
+
+    /** 打断忽略关键字列表 */
+    @Excel(name = "打断忽略关键字列表")
+    private String interruptIgnoreKeywords;
+
+    /** 客户意向提示词 */
+    @Excel(name = "客户意向提示词")
+    private String intentionTips;
+
+    /** 模型并发数 */
+    @Excel(name = "模型并发数")
+    private Long concurrentNum;
+
+    /** 一个用于启动向人工座席转移的单位数字代码。 */
+    @Excel(name = "一个用于启动向人工座席转移的单位数字代码。")
+    private String transferManualDigit;
+
+    /** 知识库分类ID */
+    @Excel(name = "知识库分类ID")
+    private Long kbCatId;
+
+    /** 远程大模型ID */
+    @Excel(name = "远程大模型ID")
+    private Long remoteLlmAgentAccountId;
+
+
+}

+ 97 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallOutboundCdr.java

@@ -0,0 +1,97 @@
+package com.fs.aiSipCall.domain;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * aiSIP手动外呼通话记录对象 ai_sip_call_outbound_cdr
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+@Data
+public class AiSipCallOutboundCdr implements Serializable {
+
+    /** $column.columnComment */
+    private String id;
+
+    /** 主叫 */
+    @Excel(name = "主叫")
+    private String caller;
+
+    /** 工号 */
+    @Excel(name = "工号")
+    private String opnum;
+
+    /** 被叫 */
+    @Excel(name = "被叫")
+    private String callee;
+
+    /** 外呼开始时间 */
+    @Excel(name = "外呼开始时间")
+    private Long startTime;
+
+    /** 被叫接听时间 */
+    @Excel(name = "被叫接听时间")
+    private Long answeredTime;
+
+    /** 挂断时间 */
+    @Excel(name = "挂断时间")
+    private Long endTime;
+
+    /** 通话UUID */
+    @Excel(name = "通话UUID")
+    private String uuid;
+
+    /** 音频或视频通话 */
+    @Excel(name = "音频或视频通话")
+    private String callType;
+
+    /** 通话时长 */
+    @Excel(name = "通话时长")
+    private Long timeLen;
+
+    /** 有效通话时长 */
+    @Excel(name = "有效通话时长")
+    private Long timeLenValid;
+
+    /** 录音文件名 */
+    @Excel(name = "录音文件名")
+    private String recordFilename;
+
+    /** 对话内容 */
+    @Excel(name = "对话内容")
+    private String chatContent;
+
+    /** 挂断原因 */
+    @Excel(name = "挂断原因")
+    private String hangupCause;
+
+    /** 通话总时长起止 */
+    private Long timeLenStart;
+    private Long timeLenEnd;
+    /** 外呼时间起止 */
+    private String startTimeStart;
+    private String startTimeEnd;
+    private Long startTimeStartLong;
+    private Long startTimeEndLong;
+
+    /** 接听时间起止 */
+    private String answeredTimeStart;
+    private String answeredTimeEnd;
+    private Long answeredTimeStartLong;
+    private Long answeredTimeEndLong;
+    /** 挂机时间起止 */
+    private String endTimeStart;
+    private String endTimeEnd;
+    private Long endTimeStartLong;
+    private Long endTimeEndLong;
+
+    private String startTimeStr;
+    private String answeredTimeStr;
+    private String endTimeStr;
+    private String timeLenSec;
+    private String timeLenValidStr;
+}

+ 270 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallPhone.java

@@ -0,0 +1,270 @@
+package com.fs.aiSipCall.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * aiSIP外呼通话记录对象 ai_sip_call_phone
+ * AI外呼扫描cc_call_phone这个表,
+ * 人工外呼cc_outbound_cdr这个表,
+ * 呼入是cc_inbound_cdr这个表
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class AiSipCallPhone implements Serializable {
+
+    /** $column.columnComment */
+    private String id;
+
+    /** 任务批次id */
+    @Excel(name = "任务批次id")
+    private Long batchId;
+    /** 批次名称 */
+    @Excel(name = "批次名称", readConverterExp = "批次名称")
+    private String batchName;
+
+    /** $column.columnComment */
+    @Excel(name = "任务批次id")
+    private String telephone;
+
+    /** 客户称呼 */
+    @Excel(name = "客户称呼")
+    private String custName;
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "创建时间")
+    private Date createTime;
+
+    /** 0. 未拨打
+        1. 已进入呼叫队列
+        2. 正在拨号
+        3. 未接通(如果关闭空号检测)
+        4. 已接通
+        5. 通话中断(转人工前挂断)
+        6. 成功转人工或AI
+        7. 线路故障
+        30. 未接通
+        31. 客户正在通话中
+        32. 关机
+        33. 空号
+        34. 无人接听
+        35. 停机
+        36. 网络忙
+        37. 语音助手
+        38. 暂时无法接通
+        39. 呼叫限制
+     */
+    @Excel(name = "呼叫状态", readConverterExp = "如果关闭空号检测")
+    private Long callstatus;
+
+    /** 外呼时间 */
+    @Excel(name = "外呼时间")
+    private Long calloutTime;
+
+    /** 呼叫次数 */
+    @Excel(name = "呼叫次数")
+    private Long callcount;
+
+    /** 呼叫结束时间 */
+    @Excel(name = "呼叫结束时间")
+    private Long callEndTime;
+
+    /** 通话时长; 秒; */
+    @Excel(name = "通话时长; 秒;")
+    private Long timeLen;
+
+    /** 人工接听的通话时长; 秒 */
+    @Excel(name = "人工接听的通话时长; 秒")
+    private Long validTimeLen;
+
+    /** 通话唯一标志 */
+    @Excel(name = "通话唯一标志")
+    private String uuid;
+
+    /** 通话接通时间 */
+    @Excel(name = "通话接通时间")
+    private Long connectedTime;
+
+    /** 挂机原因 */
+    @Excel(name = "挂机原因")
+    private String hangupCause;
+
+    /** 人工坐席应答时间 */
+    @Excel(name = "人工坐席应答时间")
+    private Long answeredTime;
+
+    /** 对话内容 */
+    @Excel(name = "对话内容")
+    private String dialogue;
+
+    /** 全程通话录音文件名 */
+    @Excel(name = "全程通话录音文件名")
+    private String wavfile;
+
+
+
+    /** 录音文件路径前缀 */
+    @Excel(name = "录音文件路径前缀")
+    private String recordServerUrl;
+
+    /** 业务json数据 */
+    @Excel(name = "业务json数据")
+    private String bizJson;
+
+    /** 交互轮次(一问一答算一轮交互) */
+    @Excel(name = "交互轮次", readConverterExp = "一=问一答算一轮交互")
+    private Long dialogueCount;
+
+    /** 人工坐席工号 */
+    @Excel(name = "人工坐席工号")
+    private String acdOpnum;
+
+    /** 加入转人工排队的时间;  */
+    @Excel(name = "加入转人工排队的时间; ")
+    private Long acdQueueTime;
+
+    /** 人工排队等待时长,秒 */
+    @Excel(name = "人工排队等待时长,秒")
+    private Long acdWaitTime;
+
+    /** 语音通话通知的TTS文本 */
+    @Excel(name = "语音通话通知的TTS文本")
+    private String ttsText;
+
+    /** $column.columnComment */
+    @Excel(name = "语音通话通知的TTS文本")
+    private String emptyNumberDetectionText;
+
+    /** 客户意向 */
+    @Excel(name = "客户意向")
+    private String intent;
+
+    /** asr时长(秒) */
+    @Excel(name = "asr时长", readConverterExp = "秒=")
+    private Long asrSeconds;
+
+    /** tts调用次数(次) */
+    @Excel(name = "tts调用次数", readConverterExp = "次=")
+    private Long ttsTimes;
+
+    /** 大模型tts的字符数(字符) */
+    @Excel(name = "大模型tts的字符数", readConverterExp = "字=符")
+    private Long ttsFlowTokens;
+
+    /** 总输入token数 */
+    @Excel(name = "总输入token数")
+    private Long inputTokens;
+
+    /** 总输出token数 */
+    @Excel(name = "总输出token数")
+    private Long outputTokens;
+
+    /** 总调用费用(asr+tts+大模型) */
+    @Excel(name = "总调用费用", readConverterExp = "a=sr+tts+大模型")
+    private BigDecimal totalCost;
+
+    /** 计费状态(1:已计费、0:未计费) */
+    @Excel(name = "计费状态", readConverterExp = "1=:已计费、0:未计费")
+    private Long billingStatus;
+
+    /** 主叫号码 */
+    @Excel(name = "主叫号码")
+    private String callerNumber;
+
+    /** 客户输入的DTMF按键 */
+    @Excel(name = "客户输入的DTMF按键")
+    private String ivrDtmfDigits;
+
+    /** 人工坐席应答时间 */
+    @Excel(name = "人工坐席应答时间")
+    private Long manualAnsweredTime;
+
+    /** 人工坐席服务时长 */
+    @Excel(name = "人工坐席服务时长")
+    private Long manualAnsweredTimeLen;
+    /** 录音文件url访问地址 */
+    private String wavFileUrl;
+
+    /** 通话总时长起止 */
+    private Long timeLenStart;
+    private Long timeLenEnd;
+    /** 外呼时间起止 */
+    private String calloutTimeStart;
+    private String calloutTimeEnd;
+    private Long calloutTimeStartLong;
+    private Long calloutTimeEndLong;
+    /** 接听时间起止 */
+    private String answeredTimeStart;
+    private String answeredTimeEnd;
+    private Long answeredTimeStartLong;
+    private Long answeredTimeEndLong;
+    /** 挂机时间起止 */
+    private String callEndTimeStart;
+    private String callEndTimeEnd;
+    private Long callEndTimeStartLong;
+    private Long callEndTimeEndLong;
+
+    private String callstatusName;
+    private String calloutTimeStr;
+    private String answeredTimeStr;
+    private String callEndTimeStr;
+    private String timeLenSec;
+
+
+    /**
+     * 根据呼叫状态码获取状态名称
+     * @param status 状态码
+     * @return 状态名称
+     */
+    public static String getCallStatusName(Long status) {
+        if (status == null) {
+            return "未知";
+        }
+        switch (status.intValue()) {
+            case 0:
+                return "未拨打";
+            case 1:
+                return "已进入呼叫队列";
+            case 2:
+                return "正在拨号";
+            case 3:
+            case 30:
+                return "未接通";
+            case 4:
+                return "已接通";
+            case 5:
+                return "通话中断";
+            case 6:
+                return "成功转人工或 AI";
+            case 7:
+                return "线路故障";
+            case 31:
+                return "客户正在通话中";
+            case 32:
+                return "关机";
+            case 33:
+                return "空号";
+            case 34:
+                return "无人接听";
+            case 35:
+                return "停机";
+            case 36:
+                return "网络忙";
+            case 37:
+                return "语音助手";
+            case 38:
+                return "暂时无法接通";
+            case 39:
+                return "呼叫限制";
+            default:
+                return "未知状态";
+        }
+    }
+}

+ 164 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallTask.java

@@ -0,0 +1,164 @@
+package com.fs.aiSipCall.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+/**
+ * aiSIP外呼任务对象 ai_sip_call_task
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiSipCallTask extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long batchId;
+
+    /** 外呼任务的业务组  */
+    @Excel(name = "外呼任务的业务组 ")
+    private String groupId;
+
+    /** $column.columnComment */
+    @Excel(name = "外呼任务的业务组 ")
+    private String batchName;
+
+    /** 是否启动任务, 1 启动, 0 停止 */
+    @Excel(name = "是否启动任务, 1 启动, 0 停止")
+    private Long ifcall;
+
+    /** 外呼速率 */
+    @Excel(name = "外呼速率")
+    private Double rate;
+
+    /** 当前任务最大可用外线数 */
+    @Excel(name = "当前任务最大可用外线数")
+    private Long threadNum;
+
+    /** 任务是否正在执行; */
+    @Excel(name = "任务是否正在执行;")
+    private Long executing;
+
+    /** 任务停止时间 */
+    @Excel(name = "任务停止时间")
+    private Long stopTime;
+
+    /** 任务创建者用户id */
+    @Excel(name = "任务创建者用户id")
+    private String userid;
+
+    /** 任务类型 0:人工预测外呼(暂不支持) 1:AI外呼 2:通知提醒 */
+    @Excel(name = "任务类型 0:人工预测外呼", readConverterExp = "暂=不支持")
+    private Long taskType;
+
+    /** 使用哪条线路外呼 */
+    @Excel(name = "使用哪条线路外呼")
+    private Long gatewayId;
+
+    /** 外呼任务,机器人的发音人 */
+    @Excel(name = "外呼任务,机器人的发音人")
+    private String voiceCode;
+
+    /** 外呼任务,机器人的tts提供者 */
+    @Excel(name = "外呼任务,机器人的tts提供者")
+    private String voiceSource;
+
+    /** 平均振铃时长(秒),taskType=0时必填 */
+    @Excel(name = "平均振铃时长", readConverterExp = "秒=")
+    private BigDecimal avgRingTimeLen;
+
+    /** 平均通话时长(秒),taskType=0时必填 */
+    @Excel(name = "平均通话时长", readConverterExp = "秒=")
+    private BigDecimal avgCallTalkTimeLen;
+
+    /** 平均事后处理时长(秒),taskType=0时必填 */
+    @Excel(name = "平均事后处理时长", readConverterExp = "秒=")
+    private BigDecimal avgCallEndProcessTimeLen;
+
+    /** 外呼节点 */
+    @Excel(name = "外呼节点")
+    private String callNodeNo;
+
+    /** 大模型底座账号的Id */
+    @Excel(name = "大模型底座账号的Id")
+    private Long llmAccountId;
+
+    /** 播放次数,taskType=2时必填 */
+    @Excel(name = "播放次数,taskType=2时必填")
+    private Long playTimes;
+
+    /** asr提供者 (aliyun/doubao/microsoft) */
+    @Excel(name = "asr提供者 ", readConverterExp = "a=liyun/doubao/microsoft")
+    private String asrProvider;
+
+    /** 转人工方式:acd、extension、gateway */
+    @Excel(name = "转人工方式:acd、extension、gateway")
+    private String aiTransferType;
+
+    /** 特定转人工方式的数据 */
+    @Excel(name = "特定转人工方式的数据")
+    private String aiTransferData;
+
+    /** 是否自动停止(1:自动停止,0:实时任务不自动停止) */
+    @Excel(name = "是否自动停止", readConverterExp = "1=:自动停止,0:实时任务不自动停止")
+    private Long autoStop;
+
+    /** IVR id */
+    @Excel(name = "IVR id")
+    private String ivrId;
+
+    /** 远程任务ID */
+    @Excel(name = "远程任务ID")
+    private Long remoteBatchId;
+
+    private Long companyId;
+    private Long companyUserId;
+
+    /** 总名单量 */
+    private Integer phoneCount;
+
+    /** 未拨打名单量 */
+    private Integer noCallCount;
+
+    /** 已拨打名单量 */
+    private Integer callCount;
+
+    /** 接通名单量 */
+    private Integer connectCount;
+
+    /** 未接通名单量 */
+    private Integer noConnectCount;
+
+    /** 实际接通率 */
+    private Double realConnectRate;
+    /**
+     * 业务组
+     */
+    private String aiTransferGroupId;
+    /**
+     * 转接分机(多个用空格分割)
+     */
+    private String aiTransferExtNumber;
+    /**
+     * 转网关
+     */
+    private String aiTransferGatewayId;
+    /**
+     * 转接号码
+     */
+    private String aiTransferGatewayDestNumber;
+    /**
+     * 预估接通率
+     */
+    private Integer conntectRate;
+    /**
+     * 创建时间区间
+     */
+    private String createTimeStart;
+    private String createTimeEnd;
+}

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

@@ -0,0 +1,99 @@
+package com.fs.aiSipCall.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * sip用户信息对象 ai_sip_call_user
+ *
+ * @author fs
+ * @date 2026-03-13
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiSipCallUser extends BaseEntity{
+
+    private static final long serialVersionUID = 1L;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 部门ID */
+    @Excel(name = "部门ID")
+    private Long deptId;
+
+    /** 登录账号 */
+    @Excel(name = "登录账号")
+    private String loginName;
+
+    /** 用户昵称 */
+    @Excel(name = "用户昵称")
+    private String userName;
+
+    /** 用户类型(00系统用户 01注册用户) */
+    @Excel(name = "用户类型", readConverterExp = "0=0系统用户,0=1注册用户")
+    private String userType;
+
+    /** 用户邮箱 */
+    @Excel(name = "用户邮箱")
+    private String email;
+
+    /** 手机号码 */
+    @Excel(name = "手机号码")
+    private String phonenumber;
+
+    /** 用户性别(0男 1女 2未知) */
+    @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
+    private String sex;
+
+    /** 头像路径 */
+    @Excel(name = "头像路径")
+    private String avatar;
+
+    /** 密码 */
+    @Excel(name = "密码")
+    private String password;
+
+    /** 盐加密 */
+    @Excel(name = "盐加密")
+    private String salt;
+
+    /** 帐号状态(0正常 1停用) */
+    @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
+    private String status;
+
+    /** 删除标志(0代表存在 2代表删除) */
+    private String delFlag;
+
+    /** 最后登录IP */
+    @Excel(name = "最后登录IP")
+    private String loginIp;
+
+    /** 最后登录时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date loginDate;
+
+    /** 密码最后更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "密码最后更新时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date pwdUpdateDate;
+
+    /** 用户自定义logo */
+    @Excel(name = "用户自定义logo")
+    private String logo;
+
+    /** 绑定的分机号 */
+    @Excel(name = "绑定的分机号")
+    private Long extNum;
+
+    private Long companyId;
+    private Long companyUserId;
+
+
+}

+ 50 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallVoiceTtsAliyun.java

@@ -0,0 +1,50 @@
+package com.fs.aiSipCall.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * aiSIP外呼阿里云音色对象 ai_sip_call_voice_tts_aliyun
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AiSipCallVoiceTtsAliyun extends BaseEntity{
+
+    /** 主键id */
+    private Long id;
+
+    /** tts发音人名称 */
+    @Excel(name = "tts发音人名称")
+    private String voiceName;
+
+    /** tts发音人代码 */
+    @Excel(name = "tts发音人代码")
+    private String voiceCode;
+
+    /** 是否启用 */
+    @Excel(name = "是否启用")
+    private Long voiceEnabled;
+
+    /** 声音源,aliyun_tts、aliyun_tts_flow */
+    @Excel(name = "声音源,aliyun_tts、aliyun_tts_flow")
+    private String voiceSource;
+
+    /** 显示优先级。数字越小,显示越靠前 */
+    @Excel(name = "显示优先级。数字越小,显示越靠前")
+    private Long priority;
+
+    /** aliyun、doubao、microsoft */
+    @Excel(name = "aliyun、doubao、microsoft")
+    private String provider;
+
+    /** 远程音色ID */
+    @Excel(name = "远程音色ID")
+    private Long remoteVoiceTtsAliyunId;
+
+
+}

+ 55 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/CcCustCallRecord.java

@@ -0,0 +1,55 @@
+package com.fs.aiSipCall.domain;
+
+//import cn.afterturn.easypoi.excel.annotation.Excel;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 沟通记录对象 cc_cust_call_record
+ * 
+ * @author ruoyi
+ * @date 2025-01-03
+ */
+@Data
+@Accessors(chain = true)
+public class CcCustCallRecord implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Long id;
+
+    /** 通话id */
+    @Excel(name = "通话id")
+    private String uuid;
+
+    /** 1:呼入,2:外呼 */
+    @Excel(name = "1:呼入,2:外呼")
+    private Integer callType;
+
+    /** 对应客户id */
+    @Excel(name = "对应客户id")
+    private Long custId;
+
+    /** 沟通内容 */
+    @Excel(name = "沟通内容")
+    private String notes;
+
+    /** 坐席用户工号 */
+    @Excel(name = "坐席用户工号")
+    private String userId;
+
+    /** 坐席姓名 */
+    @Excel(name = "坐席姓名")
+    private String userRealName;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createTime;
+
+
+}

+ 86 - 0
fs-service/src/main/java/com/fs/aiSipCall/domain/CcCustInfo.java

@@ -0,0 +1,86 @@
+package com.fs.aiSipCall.domain;
+
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 客户信息对象 cc_cust_info
+ * 
+ * @author ruoyi
+ * @date 2025-01-03
+ */
+@Data
+@Accessors(chain = true)
+public class CcCustInfo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Long id;
+
+    /** 姓名 */
+    @Excel(name = "姓名")
+    private String custName;
+
+    /** 省 */
+    @Excel(name = "省")
+    private String province;
+
+    /** 市 */
+    @Excel(name = "市")
+    private String city;
+
+    /** 县 */
+    @Excel(name = "县")
+    private String county;
+
+    /** 省编号 */
+    @Excel(name = "省编号")
+    private String provinceCode;
+
+    /** 市编号 */
+    @Excel(name = "市编号")
+    private String cityCode;
+
+    /** 县编号 */
+    @Excel(name = "县编号")
+    private String countyCode;
+
+    /** 详细地址 */
+    @Excel(name = "详细地址")
+    private String address;
+
+    /** 性别(0男,1女) */
+    @Excel(name = "性别")
+    private Integer gender;
+
+    /** 号码 */
+    @Excel(name = "号码")
+    private String phoneNum;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updateTime;
+
+    // 我的工作台弹屏保存需要带的内容
+    // CcCustCallRecord
+    private String callRecord; // 沟通内容
+
+    // 详情带的参数
+    private List<CcCustCallRecord> callRecordList;
+    //登陆账号
+    private String opNum;
+    //用户名称
+    private String userName;
+
+}

+ 16 - 0
fs-service/src/main/java/com/fs/aiSipCall/dto/AiCallListModel.java

@@ -0,0 +1,16 @@
+package com.fs.aiSipCall.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @Author:peicj
+ * @Description: 导入接口入参
+ * @Date:2026/3/10 9:16
+ */
+@Data
+public class AiCallListModel {
+    private Long batchId; // 任务id
+    private List<String> phoneList; // 号码列表
+}

+ 24 - 0
fs-service/src/main/java/com/fs/aiSipCall/dto/CallTaskStatModel.java

@@ -0,0 +1,24 @@
+package com.fs.aiSipCall.dto;
+
+import lombok.Data;
+
+/**
+ * @Author:peicj
+ * @Description: 电话统计参数
+ * @Date:2026/3/10 11:09
+ */
+@Data
+public class CallTaskStatModel {
+
+    private Long batchId;
+
+    /** 总名单量 */
+    private Integer phoneCount = 0;
+
+    /** 已拨打名单量 */
+    private Integer callCount = 0;
+
+    /** 接通名单量 */
+    private Integer connectCount = 0;
+
+}

+ 14 - 0
fs-service/src/main/java/com/fs/aiSipCall/dto/CommonCallListModel.java

@@ -0,0 +1,14 @@
+package com.fs.aiSipCall.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * AI外呼的
+ */
+@Data
+public class CommonCallListModel {
+    private Long batchId; // 任务id
+    private List<CommonPhoneModel> phoneList; // 号码列表
+}

+ 13 - 0
fs-service/src/main/java/com/fs/aiSipCall/dto/CommonPhoneModel.java

@@ -0,0 +1,13 @@
+package com.fs.aiSipCall.dto;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+
+@Data
+public class CommonPhoneModel {
+
+    private String phoneNum; // 手机号码
+    private String noticeContent; // 提醒内容
+    private JSONObject bizJson; // 随路数据(键值对)
+
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallBizGroupMapper.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.aiSipCall.domain.AiSipCallBizGroup;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼技能组Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface AiSipCallBizGroupMapper extends BaseMapper<AiSipCallBizGroup>{
+    /**
+     * 查询aiSIP外呼技能组
+     * 
+     * @param groupId aiSIP外呼技能组主键
+     * @return aiSIP外呼技能组
+     */
+    AiSipCallBizGroup selectAiSipCallBizGroupByGroupId(Long groupId);
+
+    /**
+     * 查询aiSIP外呼技能组列表
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return aiSIP外呼技能组集合
+     */
+    List<AiSipCallBizGroup> selectAiSipCallBizGroupList(AiSipCallBizGroup aiSipCallBizGroup);
+
+    /**
+     * 新增aiSIP外呼技能组
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return 结果
+     */
+    int insertAiSipCallBizGroup(AiSipCallBizGroup aiSipCallBizGroup);
+
+    /**
+     * 修改aiSIP外呼技能组
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return 结果
+     */
+    int updateAiSipCallBizGroup(AiSipCallBizGroup aiSipCallBizGroup);
+
+    /**
+     * 删除aiSIP外呼技能组
+     * 
+     * @param groupId aiSIP外呼技能组主键
+     * @return 结果
+     */
+    int deleteAiSipCallBizGroupByGroupId(Long groupId);
+
+    /**
+     * 批量删除aiSIP外呼技能组
+     * 
+     * @param groupIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallBizGroupByGroupIds(Long[] groupIds);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallGatewayMapper.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.aiSipCall.domain.AiSipCallGateway;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼网关Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface AiSipCallGatewayMapper extends BaseMapper<AiSipCallGateway>{
+    /**
+     * 查询aiSIP外呼网关
+     * 
+     * @param id aiSIP外呼网关主键
+     * @return aiSIP外呼网关
+     */
+    AiSipCallGateway selectAiSipCallGatewayById(Long id);
+
+    /**
+     * 查询aiSIP外呼网关列表
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return aiSIP外呼网关集合
+     */
+    List<AiSipCallGateway> selectAiSipCallGatewayList(AiSipCallGateway aiSipCallGateway);
+
+    /**
+     * 新增aiSIP外呼网关
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return 结果
+     */
+    int insertAiSipCallGateway(AiSipCallGateway aiSipCallGateway);
+
+    /**
+     * 修改aiSIP外呼网关
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return 结果
+     */
+    int updateAiSipCallGateway(AiSipCallGateway aiSipCallGateway);
+
+    /**
+     * 删除aiSIP外呼网关
+     * 
+     * @param id aiSIP外呼网关主键
+     * @return 结果
+     */
+    int deleteAiSipCallGatewayById(Long id);
+
+    /**
+     * 批量删除aiSIP外呼网关
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallGatewayByIds(Long[] ids);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallLlmAgentAccountMapper.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.aiSipCall.domain.AiSipCallLlmAgentAccount;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼大模型Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface AiSipCallLlmAgentAccountMapper extends BaseMapper<AiSipCallLlmAgentAccount>{
+    /**
+     * 查询aiSIP外呼大模型
+     * 
+     * @param id aiSIP外呼大模型主键
+     * @return aiSIP外呼大模型
+     */
+    AiSipCallLlmAgentAccount selectAiSipCallLlmAgentAccountById(Long id);
+
+    /**
+     * 查询aiSIP外呼大模型列表
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return aiSIP外呼大模型集合
+     */
+    List<AiSipCallLlmAgentAccount> selectAiSipCallLlmAgentAccountList(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount);
+
+    /**
+     * 新增aiSIP外呼大模型
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return 结果
+     */
+    int insertAiSipCallLlmAgentAccount(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount);
+
+    /**
+     * 修改aiSIP外呼大模型
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return 结果
+     */
+    int updateAiSipCallLlmAgentAccount(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount);
+
+    /**
+     * 删除aiSIP外呼大模型
+     * 
+     * @param id aiSIP外呼大模型主键
+     * @return 结果
+     */
+    int deleteAiSipCallLlmAgentAccountById(Long id);
+
+    /**
+     * 批量删除aiSIP外呼大模型
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallLlmAgentAccountByIds(Long[] ids);
+}

+ 71 - 0
fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallOutboundCdrMapper.java

@@ -0,0 +1,71 @@
+package com.fs.aiSipCall.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.aiSipCall.domain.AiSipCallOutboundCdr;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * aiSIP手动外呼通话记录Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface AiSipCallOutboundCdrMapper extends BaseMapper<AiSipCallOutboundCdr>{
+    /**
+     * 查询aiSIP手动外呼通话记录
+     * 
+     * @param id aiSIP手动外呼通话记录主键
+     * @return aiSIP手动外呼通话记录
+     */
+    AiSipCallOutboundCdr selectAiSipCallOutboundCdrById(String id);
+
+    /**
+     * 查询aiSIP手动外呼通话记录列表
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return aiSIP手动外呼通话记录集合
+     */
+    List<AiSipCallOutboundCdr> selectAiSipCallOutboundCdrList(AiSipCallOutboundCdr aiSipCallOutboundCdr);
+
+    /**
+     * 新增aiSIP手动外呼通话记录
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return 结果
+     */
+    int insertAiSipCallOutboundCdr(AiSipCallOutboundCdr aiSipCallOutboundCdr);
+
+    /**
+     * 修改aiSIP手动外呼通话记录
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return 结果
+     */
+    int updateAiSipCallOutboundCdr(AiSipCallOutboundCdr aiSipCallOutboundCdr);
+
+    /**
+     * 删除aiSIP手动外呼通话记录
+     * 
+     * @param id aiSIP手动外呼通话记录主键
+     * @return 结果
+     */
+    int deleteAiSipCallOutboundCdrById(String id);
+
+    /**
+     * 批量删除aiSIP手动外呼通话记录
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallOutboundCdrByIds(String[] ids);
+
+    /**
+     * 查询当天的外呼记录 ID 列表
+     * @return 当天的外呼记录 ID 列表
+     */
+    @Select("SELECT * FROM ai_sip_call_outbound_cdr WHERE start_time >= #{startTime} AND start_time <= #{endTime}")
+    List<AiSipCallOutboundCdr> selectCurrentDayCallRecords(@Param("startTime") Long startTime, @Param("endTime") Long endTime);
+}

+ 96 - 0
fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallPhoneMapper.java

@@ -0,0 +1,96 @@
+package com.fs.aiSipCall.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.aiSipCall.domain.AiSipCallPhone;
+import com.fs.aiSipCall.dto.CallTaskStatModel;
+import com.fs.aiSipCall.dto.CommonPhoneModel;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼通话记录Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface AiSipCallPhoneMapper extends BaseMapper<AiSipCallPhone>{
+    /**
+     * 查询aiSIP外呼通话记录
+     * 
+     * @param id aiSIP外呼通话记录主键
+     * @return aiSIP外呼通话记录
+     */
+    AiSipCallPhone selectAiSipCallPhoneById(String id);
+
+    /**
+     * 查询aiSIP外呼通话记录列表
+     * 
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return aiSIP外呼通话记录集合
+     */
+    List<AiSipCallPhone> selectAiSipCallPhoneList(AiSipCallPhone aiSipCallPhone);
+
+    /**
+     * 新增aiSIP外呼通话记录
+     * 
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return 结果
+     */
+    int insertAiSipCallPhone(AiSipCallPhone aiSipCallPhone);
+
+    /**
+     * 修改aiSIP外呼通话记录
+     * 
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return 结果
+     */
+    int updateAiSipCallPhone(AiSipCallPhone aiSipCallPhone);
+
+    /**
+     * 删除aiSIP外呼通话记录
+     * 
+     * @param id aiSIP外呼通话记录主键
+     * @return 结果
+     */
+    int deleteAiSipCallPhoneById(String id);
+
+    /**
+     * 批量删除aiSIP外呼通话记录
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallPhoneByIds(String[] ids);
+    /**
+     * 根据batchId统计外呼数据
+     * @param batchId 任务id
+     * @return 结果
+     */
+    CallTaskStatModel statByBatchId(Long batchId);
+
+    /**
+     * 验证是否重复录入
+     * @param batchId   任务Id
+     * @param phoneList 电话号码
+     * @return List<String> 重复的电话
+     */
+    @Select("<script>" +
+            "SELECT DISTINCT telephone FROM ai_sip_call_phone " +
+            "WHERE batch_id = #{batchId} " +
+            "AND callstatus = 0 " +
+            "AND telephone IN " +
+            "<foreach item='phone' collection='phoneList' open='(' separator=',' close=')'>" +
+            "#{phone.phoneNum}" +
+            "</foreach>" +
+            "</script>")
+    List<String> isDuplicateEntry(@Param("batchId") Long batchId,@Param("phoneList")  List<CommonPhoneModel> phoneList);
+
+    /**
+     * 查询当天的外呼记录 ID 列表
+     * @return 当天的外呼记录 ID 列表
+     */
+    @Select("SELECT * FROM ai_sip_call_phone WHERE callout_time >= #{startTime} AND callout_time <= #{endTime}")
+    List<AiSipCallPhone> selectCurrentDayCallRecords(@Param("startTime") Long startTime, @Param("endTime") Long endTime);
+}

+ 67 - 0
fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallTaskMapper.java

@@ -0,0 +1,67 @@
+package com.fs.aiSipCall.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.aiSipCall.domain.AiSipCallTask;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼任务Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface AiSipCallTaskMapper extends BaseMapper<AiSipCallTask>{
+    /**
+     * 查询aiSIP外呼任务
+     * 
+     * @param batchId aiSIP外呼任务主键
+     * @return aiSIP外呼任务
+     */
+    AiSipCallTask selectAiSipCallTaskByBatchId(Long batchId);
+
+    /**
+     * 查询aiSIP外呼任务列表
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return aiSIP外呼任务集合
+     */
+    List<AiSipCallTask> selectAiSipCallTaskList(AiSipCallTask aiSipCallTask);
+
+    /**
+     * 新增aiSIP外呼任务
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return 结果
+     */
+    int insertAiSipCallTask(AiSipCallTask aiSipCallTask);
+
+    /**
+     * 修改aiSIP外呼任务
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return 结果
+     */
+    int updateAiSipCallTask(AiSipCallTask aiSipCallTask);
+
+    /**
+     * 删除aiSIP外呼任务
+     * 
+     * @param batchId aiSIP外呼任务主键
+     * @return 结果
+     */
+    int deleteAiSipCallTaskByBatchId(Long batchId);
+
+    /**
+     * 批量删除aiSIP外呼任务
+     * 
+     * @param batchIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallTaskByBatchIds(Long[] batchIds);
+
+    @Select("select * from ai_sip_call_task where remote_batch_id = #{remoteBatchId} limit 1")
+    AiSipCallTask selectAiSipCallTaskByRemoteBatchId(@Param("remoteBatchId") Long remoteBatchId);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallUserMapper.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.aiSipCall.domain.AiSipCallUser;
+
+import java.util.List;
+
+/**
+ * sip用户信息Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-13
+ */
+public interface AiSipCallUserMapper extends BaseMapper<AiSipCallUser>{
+    /**
+     * 查询sip用户信息
+     * 
+     * @param userId sip用户信息主键
+     * @return sip用户信息
+     */
+    AiSipCallUser selectAiSipCallUserByUserId(Long userId);
+
+    /**
+     * 查询sip用户信息列表
+     * 
+     * @param aiSipCallUser sip用户信息
+     * @return sip用户信息集合
+     */
+    List<AiSipCallUser> selectAiSipCallUserList(AiSipCallUser aiSipCallUser);
+
+    /**
+     * 新增sip用户信息
+     * 
+     * @param aiSipCallUser sip用户信息
+     * @return 结果
+     */
+    int insertAiSipCallUser(AiSipCallUser aiSipCallUser);
+
+    /**
+     * 修改sip用户信息
+     * 
+     * @param aiSipCallUser sip用户信息
+     * @return 结果
+     */
+    int updateAiSipCallUser(AiSipCallUser aiSipCallUser);
+
+    /**
+     * 删除sip用户信息
+     * 
+     * @param userId sip用户信息主键
+     * @return 结果
+     */
+    int deleteAiSipCallUserByUserId(Long userId);
+
+    /**
+     * 批量删除sip用户信息
+     * 
+     * @param userIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallUserByUserIds(Long[] userIds);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallVoiceTtsAliyunMapper.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.aiSipCall.domain.AiSipCallVoiceTtsAliyun;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼阿里云音色Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface AiSipCallVoiceTtsAliyunMapper extends BaseMapper<AiSipCallVoiceTtsAliyun>{
+    /**
+     * 查询aiSIP外呼阿里云音色
+     * 
+     * @param id aiSIP外呼阿里云音色主键
+     * @return aiSIP外呼阿里云音色
+     */
+    AiSipCallVoiceTtsAliyun selectAiSipCallVoiceTtsAliyunById(Long id);
+
+    /**
+     * 查询aiSIP外呼阿里云音色列表
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return aiSIP外呼阿里云音色集合
+     */
+    List<AiSipCallVoiceTtsAliyun> selectAiSipCallVoiceTtsAliyunList(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun);
+
+    /**
+     * 新增aiSIP外呼阿里云音色
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return 结果
+     */
+    int insertAiSipCallVoiceTtsAliyun(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun);
+
+    /**
+     * 修改aiSIP外呼阿里云音色
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return 结果
+     */
+    int updateAiSipCallVoiceTtsAliyun(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun);
+
+    /**
+     * 删除aiSIP外呼阿里云音色
+     * 
+     * @param id aiSIP外呼阿里云音色主键
+     * @return 结果
+     */
+    int deleteAiSipCallVoiceTtsAliyunById(Long id);
+
+    /**
+     * 批量删除aiSIP外呼阿里云音色
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallVoiceTtsAliyunByIds(Long[] ids);
+}

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

@@ -0,0 +1,21 @@
+package com.fs.aiSipCall.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class ApiCallRecordByUuidQueryParams implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 通话uuid */
+    private String uuid;
+
+    /** 01呼入 02AI外呼 03人工外呼 */
+    private String callType;
+
+    /**
+     * 2:执行成功,3:执行失败
+     */
+    private Integer status;
+}

+ 36 - 0
fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordQueryParams.java

@@ -0,0 +1,36 @@
+package com.fs.aiSipCall.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class ApiCallRecordQueryParams implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private Integer pageNum;
+    private Integer pageSize;
+
+    /** 类型(01:呼入, 02:AI外呼, 03:人工外呼) */
+    private String callType;
+    /** 任务ID */
+    private Long batchId;
+
+    /** 呼入caller/AI外呼telephone/人工外呼callee */
+    private String telephone;
+
+    /** 通话总时长起止 */
+    private Integer timeLenStart;
+    private Integer timeLenEnd;
+    /** 外呼时间起止 */
+    private String calloutTimeStart;
+    private String calloutTimeEnd;
+    /** 接听时间起止 */
+    private String answeredTimeStart;
+    private String answeredTimeEnd;
+    /** 挂机时间起止 */
+    private String callEndTimeStart;
+    private String callEndTimeEnd;
+    /** 分机号 */
+    private String extnum;
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallBizGroupService.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aiSipCall.domain.AiSipCallBizGroup;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼技能组Service接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface IAiSipCallBizGroupService extends IService<AiSipCallBizGroup>{
+    /**
+     * 查询aiSIP外呼技能组
+     * 
+     * @param groupId aiSIP外呼技能组主键
+     * @return aiSIP外呼技能组
+     */
+    AiSipCallBizGroup selectAiSipCallBizGroupByGroupId(Long groupId);
+
+    /**
+     * 查询aiSIP外呼技能组列表
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return aiSIP外呼技能组集合
+     */
+    List<AiSipCallBizGroup> selectAiSipCallBizGroupList(AiSipCallBizGroup aiSipCallBizGroup);
+
+    /**
+     * 新增aiSIP外呼技能组
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return 结果
+     */
+    int insertAiSipCallBizGroup(AiSipCallBizGroup aiSipCallBizGroup);
+
+    /**
+     * 修改aiSIP外呼技能组
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return 结果
+     */
+    int updateAiSipCallBizGroup(AiSipCallBizGroup aiSipCallBizGroup);
+
+    /**
+     * 批量删除aiSIP外呼技能组
+     * 
+     * @param groupIds 需要删除的aiSIP外呼技能组主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallBizGroupByGroupIds(Long[] groupIds);
+
+    /**
+     * 删除aiSIP外呼技能组信息
+     * 
+     * @param groupId aiSIP外呼技能组主键
+     * @return 结果
+     */
+    int deleteAiSipCallBizGroupByGroupId(Long groupId);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallGatewayService.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aiSipCall.domain.AiSipCallGateway;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼网关Service接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface IAiSipCallGatewayService extends IService<AiSipCallGateway>{
+    /**
+     * 查询aiSIP外呼网关
+     * 
+     * @param id aiSIP外呼网关主键
+     * @return aiSIP外呼网关
+     */
+    AiSipCallGateway selectAiSipCallGatewayById(Long id);
+
+    /**
+     * 查询aiSIP外呼网关列表
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return aiSIP外呼网关集合
+     */
+    List<AiSipCallGateway> selectAiSipCallGatewayList(AiSipCallGateway aiSipCallGateway);
+
+    /**
+     * 新增aiSIP外呼网关
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return 结果
+     */
+    int insertAiSipCallGateway(AiSipCallGateway aiSipCallGateway);
+
+    /**
+     * 修改aiSIP外呼网关
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return 结果
+     */
+    int updateAiSipCallGateway(AiSipCallGateway aiSipCallGateway);
+
+    /**
+     * 批量删除aiSIP外呼网关
+     * 
+     * @param ids 需要删除的aiSIP外呼网关主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallGatewayByIds(Long[] ids);
+
+    /**
+     * 删除aiSIP外呼网关信息
+     * 
+     * @param id aiSIP外呼网关主键
+     * @return 结果
+     */
+    int deleteAiSipCallGatewayById(Long id);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallLlmAgentAccountService.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aiSipCall.domain.AiSipCallLlmAgentAccount;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼大模型Service接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface IAiSipCallLlmAgentAccountService extends IService<AiSipCallLlmAgentAccount>{
+    /**
+     * 查询aiSIP外呼大模型
+     * 
+     * @param id aiSIP外呼大模型主键
+     * @return aiSIP外呼大模型
+     */
+    AiSipCallLlmAgentAccount selectAiSipCallLlmAgentAccountById(Long id);
+
+    /**
+     * 查询aiSIP外呼大模型列表
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return aiSIP外呼大模型集合
+     */
+    List<AiSipCallLlmAgentAccount> selectAiSipCallLlmAgentAccountList(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount);
+
+    /**
+     * 新增aiSIP外呼大模型
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return 结果
+     */
+    int insertAiSipCallLlmAgentAccount(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount);
+
+    /**
+     * 修改aiSIP外呼大模型
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return 结果
+     */
+    int updateAiSipCallLlmAgentAccount(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount);
+
+    /**
+     * 批量删除aiSIP外呼大模型
+     * 
+     * @param ids 需要删除的aiSIP外呼大模型主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallLlmAgentAccountByIds(Long[] ids);
+
+    /**
+     * 删除aiSIP外呼大模型信息
+     * 
+     * @param id aiSIP外呼大模型主键
+     * @return 结果
+     */
+    int deleteAiSipCallLlmAgentAccountById(Long id);
+}

+ 74 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallOutboundCdrService.java

@@ -0,0 +1,74 @@
+package com.fs.aiSipCall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aiSipCall.domain.AiSipCallOutboundCdr;
+import com.fs.aiSipCall.domain.CcCustInfo;
+import com.fs.aiSipCall.param.ApiCallRecordByUuidQueryParams;
+import com.fs.common.core.domain.AjaxResult;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * aiSIP手动外呼通话记录Service接口
+ * 
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface IAiSipCallOutboundCdrService extends IService<AiSipCallOutboundCdr>{
+    /**
+     * 查询aiSIP手动外呼通话记录
+     * 
+     * @param id aiSIP手动外呼通话记录主键
+     * @return aiSIP手动外呼通话记录
+     */
+    AiSipCallOutboundCdr selectAiSipCallOutboundCdrById(String id);
+
+    /**
+     * 查询aiSIP手动外呼通话记录列表
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return aiSIP手动外呼通话记录集合
+     */
+    List<AiSipCallOutboundCdr> selectAiSipCallOutboundCdrList(AiSipCallOutboundCdr aiSipCallOutboundCdr);
+
+    /**
+     * 新增aiSIP手动外呼通话记录
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return 结果
+     */
+    int insertAiSipCallOutboundCdr(AiSipCallOutboundCdr aiSipCallOutboundCdr);
+
+    /**
+     * 修改aiSIP手动外呼通话记录
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return 结果
+     */
+    int updateAiSipCallOutboundCdr(AiSipCallOutboundCdr aiSipCallOutboundCdr);
+
+    /**
+     * 批量删除aiSIP手动外呼通话记录
+     * 
+     * @param ids 需要删除的aiSIP手动外呼通话记录主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallOutboundCdrByIds(String[] ids);
+
+    /**
+     * 删除aiSIP手动外呼通话记录信息
+     * 
+     * @param id aiSIP手动外呼通话记录主键
+     * @return 结果
+     */
+    int deleteAiSipCallOutboundCdrById(String id);
+
+    AjaxResult getCustCommunicationInfo(String phoneNum, Integer callType, String uuid);
+
+    AjaxResult addCustcallrecord(CcCustInfo ccCustInfo);
+
+    CompletableFuture<String> scheduledGetCallRecord();
+
+    int syncByUuid(ApiCallRecordByUuidQueryParams req);
+}

+ 84 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallPhoneService.java

@@ -0,0 +1,84 @@
+package com.fs.aiSipCall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aiSipCall.domain.AiSipCallPhone;
+import com.fs.aiSipCall.dto.CallTaskStatModel;
+import com.fs.aiSipCall.dto.CommonPhoneModel;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * aiSIP外呼通话记录Service接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface IAiSipCallPhoneService extends IService<AiSipCallPhone>{
+    /**
+     * 查询aiSIP外呼通话记录
+     * 
+     * @param id aiSIP外呼通话记录主键
+     * @return aiSIP外呼通话记录
+     */
+    AiSipCallPhone selectAiSipCallPhoneById(String id);
+
+    /**
+     * 查询aiSIP外呼通话记录列表
+     * 
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return aiSIP外呼通话记录集合
+     */
+    List<AiSipCallPhone> selectAiSipCallPhoneList(AiSipCallPhone aiSipCallPhone);
+
+    /**
+     * 新增aiSIP外呼通话记录
+     * 
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return 结果
+     */
+    int insertAiSipCallPhone(AiSipCallPhone aiSipCallPhone);
+
+    /**
+     * 修改aiSIP外呼通话记录
+     * 
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return 结果
+     */
+    int updateAiSipCallPhone(AiSipCallPhone aiSipCallPhone);
+
+    /**
+     * 批量删除aiSIP外呼通话记录
+     * 
+     * @param ids 需要删除的aiSIP外呼通话记录主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallPhoneByIds(String[] ids);
+
+    /**
+     * 删除aiSIP外呼通话记录信息
+     * 
+     * @param id aiSIP外呼通话记录主键
+     * @return 结果
+     */
+    int deleteAiSipCallPhoneById(String id);
+    /**
+     * 根据batchId统计外呼数据
+     * @param batchId 任务id
+     * @return 结果
+     */
+    CallTaskStatModel statByBatchId(Long batchId);
+
+    /**
+     * 验证是否重复录入
+     * @param batchId   任务Id
+     * @param phoneList 电话号码
+     * @return List<String> 重复的电话
+     */
+    List<String> isDuplicateEntry(Long batchId, List<CommonPhoneModel> phoneList);
+
+    /**
+     * 拉取当天记录
+     */
+    CompletableFuture<String> scheduledGetCallRecord();
+}

+ 74 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallTaskService.java

@@ -0,0 +1,74 @@
+package com.fs.aiSipCall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aiSipCall.domain.AiSipCallTask;
+import com.fs.aiSipCall.dto.CallTaskStatModel;
+import com.fs.aiSipCall.dto.CommonPhoneModel;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼任务Service接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface IAiSipCallTaskService extends IService<AiSipCallTask>{
+    /**
+     * 查询aiSIP外呼任务
+     * 
+     * @param batchId aiSIP外呼任务主键
+     * @return aiSIP外呼任务
+     */
+    AiSipCallTask selectAiSipCallTaskByBatchId(Long batchId);
+
+    /**
+     * 查询aiSIP外呼任务列表
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return aiSIP外呼任务集合
+     */
+    List<AiSipCallTask> selectAiSipCallTaskList(AiSipCallTask aiSipCallTask);
+
+    /**
+     * 新增aiSIP外呼任务
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return 结果
+     */
+    int insertAiSipCallTask(AiSipCallTask aiSipCallTask);
+
+    /**
+     * 修改aiSIP外呼任务
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return 结果
+     */
+    int updateAiSipCallTask(AiSipCallTask aiSipCallTask);
+
+    /**
+     * 批量删除aiSIP外呼任务
+     * 
+     * @param batchIds 需要删除的aiSIP外呼任务主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallTaskByBatchIds(Long[] batchIds);
+
+    /**
+     * 删除aiSIP外呼任务信息
+     * 
+     * @param batchId aiSIP外呼任务主键
+     * @return 结果
+     */
+    int deleteAiSipCallTaskByBatchId(Long batchId);
+
+    int startTask(Long batchId);
+
+    int stopTask(Long batchId);
+
+    int commonImportExcel(Long batchId,List<CommonPhoneModel> phoneList);
+
+    CallTaskStatModel statByBatchId(Long batchId);
+
+    AiSipCallTask selectAiSipCallTaskByRemoteBatchId(Long remoteBatchId);
+}

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

@@ -0,0 +1,72 @@
+package com.fs.aiSipCall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aiSipCall.domain.AiSipCallUser;
+import com.fs.common.core.domain.AjaxResult;
+
+import java.util.List;
+
+/**
+ * sip用户信息Service接口
+ *
+ * @author fs
+ * @date 2026-03-13
+ */
+public interface IAiSipCallUserService extends IService<AiSipCallUser>{
+    /**
+     * 查询sip用户信息
+     *
+     * @param userId sip用户信息主键
+     * @return sip用户信息
+     */
+    AiSipCallUser selectAiSipCallUserByUserId(Long userId);
+
+    /**
+     * 查询sip用户信息列表
+     *
+     * @param aiSipCallUser sip用户信息
+     * @return sip用户信息集合
+     */
+    List<AiSipCallUser> selectAiSipCallUserList(AiSipCallUser aiSipCallUser);
+
+    /**
+     * 新增sip用户信息
+     *
+     * @param aiSipCallUser sip用户信息
+     * @return 结果
+     */
+    int insertAiSipCallUser(AiSipCallUser aiSipCallUser);
+
+    /**
+     * 修改sip用户信息
+     *
+     * @param aiSipCallUser sip用户信息
+     * @return 结果
+     */
+    int updateAiSipCallUser(AiSipCallUser aiSipCallUser);
+
+    /**
+     * 批量删除sip用户信息
+     *
+     * @param userIds 需要删除的sip用户信息主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallUserByUserIds(Long[] userIds);
+
+    /**
+     * 删除sip用户信息信息
+     *
+     * @param userId sip用户信息主键
+     * @return 结果
+     */
+    int deleteAiSipCallUserByUserId(Long userId);
+
+    AjaxResult getUnBindExtnum();
+
+    /**
+     * 查询aiSIP工具条基础配置参数
+     * @param extNum 分机号
+     * @return AjaxResult 结果
+     */
+    AjaxResult getToolbarBasicParam(String extNum);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallVoiceTtsAliyunService.java

@@ -0,0 +1,62 @@
+package com.fs.aiSipCall.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.aiSipCall.domain.AiSipCallVoiceTtsAliyun;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼阿里云音色Service接口
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+public interface IAiSipCallVoiceTtsAliyunService extends IService<AiSipCallVoiceTtsAliyun>{
+    /**
+     * 查询aiSIP外呼阿里云音色
+     * 
+     * @param id aiSIP外呼阿里云音色主键
+     * @return aiSIP外呼阿里云音色
+     */
+    AiSipCallVoiceTtsAliyun selectAiSipCallVoiceTtsAliyunById(Long id);
+
+    /**
+     * 查询aiSIP外呼阿里云音色列表
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return aiSIP外呼阿里云音色集合
+     */
+    List<AiSipCallVoiceTtsAliyun> selectAiSipCallVoiceTtsAliyunList(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun);
+
+    /**
+     * 新增aiSIP外呼阿里云音色
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return 结果
+     */
+    int insertAiSipCallVoiceTtsAliyun(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun);
+
+    /**
+     * 修改aiSIP外呼阿里云音色
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return 结果
+     */
+    int updateAiSipCallVoiceTtsAliyun(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun);
+
+    /**
+     * 批量删除aiSIP外呼阿里云音色
+     * 
+     * @param ids 需要删除的aiSIP外呼阿里云音色主键集合
+     * @return 结果
+     */
+    int deleteAiSipCallVoiceTtsAliyunByIds(Long[] ids);
+
+    /**
+     * 删除aiSIP外呼阿里云音色信息
+     * 
+     * @param id aiSIP外呼阿里云音色主键
+     * @return 结果
+     */
+    int deleteAiSipCallVoiceTtsAliyunById(Long id);
+}

+ 110 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallBizGroupServiceImpl.java

@@ -0,0 +1,110 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallBizGroup;
+import com.fs.aiSipCall.mapper.AiSipCallBizGroupMapper;
+import com.fs.aiSipCall.service.IAiSipCallBizGroupService;
+import com.fs.common.utils.DateUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼技能组Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@Slf4j
+@Service
+public class AiSipCallBizGroupServiceImpl extends ServiceImpl<AiSipCallBizGroupMapper, AiSipCallBizGroup> implements IAiSipCallBizGroupService {
+
+    /**
+     * 查询aiSIP外呼技能组
+     * 
+     * @param groupId aiSIP外呼技能组主键
+     * @return aiSIP外呼技能组
+     */
+    @Override
+    public AiSipCallBizGroup selectAiSipCallBizGroupByGroupId(Long groupId)
+    {
+        return baseMapper.selectAiSipCallBizGroupByGroupId(groupId);
+    }
+
+    /**
+     * 查询aiSIP外呼技能组列表
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return aiSIP外呼技能组
+     */
+    @Override
+    public List<AiSipCallBizGroup> selectAiSipCallBizGroupList(AiSipCallBizGroup aiSipCallBizGroup)
+    {
+        //先使用远程技能组
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.BUSIGROUP_LIST_API);
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                String data = jsonObject.getString("data");
+                return JSONObject.parseArray(data, AiSipCallBizGroup.class);
+            }else{
+                log.error("获取技能组接口失败:{}", jsonObject.getString("msg"));
+            }
+        }
+        return null;
+//        return baseMapper.selectAiSipCallBizGroupList(aiSipCallBizGroup);
+    }
+
+    /**
+     * 新增aiSIP外呼技能组
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return 结果
+     */
+    @Override
+    public int insertAiSipCallBizGroup(AiSipCallBizGroup aiSipCallBizGroup)
+    {
+        aiSipCallBizGroup.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertAiSipCallBizGroup(aiSipCallBizGroup);
+    }
+
+    /**
+     * 修改aiSIP外呼技能组
+     * 
+     * @param aiSipCallBizGroup aiSIP外呼技能组
+     * @return 结果
+     */
+    @Override
+    public int updateAiSipCallBizGroup(AiSipCallBizGroup aiSipCallBizGroup)
+    {
+        return baseMapper.updateAiSipCallBizGroup(aiSipCallBizGroup);
+    }
+
+    /**
+     * 批量删除aiSIP外呼技能组
+     * 
+     * @param groupIds 需要删除的aiSIP外呼技能组主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallBizGroupByGroupIds(Long[] groupIds)
+    {
+        return baseMapper.deleteAiSipCallBizGroupByGroupIds(groupIds);
+    }
+
+    /**
+     * 删除aiSIP外呼技能组信息
+     * 
+     * @param groupId aiSIP外呼技能组主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallBizGroupByGroupId(Long groupId)
+    {
+        return baseMapper.deleteAiSipCallBizGroupByGroupId(groupId);
+    }
+}

+ 112 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallGatewayServiceImpl.java

@@ -0,0 +1,112 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallGateway;
+import com.fs.aiSipCall.mapper.AiSipCallGatewayMapper;
+import com.fs.aiSipCall.service.IAiSipCallGatewayService;
+import com.fs.common.utils.DateUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼网关Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@Slf4j
+@Service
+public class AiSipCallGatewayServiceImpl extends ServiceImpl<AiSipCallGatewayMapper, AiSipCallGateway> implements IAiSipCallGatewayService {
+
+    /**
+     * 查询aiSIP外呼网关
+     * 
+     * @param id aiSIP外呼网关主键
+     * @return aiSIP外呼网关
+     */
+    @Override
+    public AiSipCallGateway selectAiSipCallGatewayById(Long id)
+    {
+        return baseMapper.selectAiSipCallGatewayById(id);
+    }
+
+    /**
+     * 查询aiSIP外呼网关列表
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return aiSIP外呼网关
+     */
+    @Override
+    public List<AiSipCallGateway> selectAiSipCallGatewayList(AiSipCallGateway aiSipCallGateway)
+    {
+        //先使用远程网关
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.GATEWAY_LIST_API);
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                String data = jsonObject.getString("data");
+                return JSONObject.parseArray(data, AiSipCallGateway.class);
+            }else{
+                log.error("获取网关接口失败:{}", jsonObject.getString("msg"));
+            }
+        }
+        return null;
+
+
+//        return baseMapper.selectAiSipCallGatewayList(aiSipCallGateway);
+    }
+
+    /**
+     * 新增aiSIP外呼网关
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return 结果
+     */
+    @Override
+    public int insertAiSipCallGateway(AiSipCallGateway aiSipCallGateway)
+    {
+        return baseMapper.insertAiSipCallGateway(aiSipCallGateway);
+    }
+
+    /**
+     * 修改aiSIP外呼网关
+     * 
+     * @param aiSipCallGateway aiSIP外呼网关
+     * @return 结果
+     */
+    @Override
+    public int updateAiSipCallGateway(AiSipCallGateway aiSipCallGateway)
+    {
+        aiSipCallGateway.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateAiSipCallGateway(aiSipCallGateway);
+    }
+
+    /**
+     * 批量删除aiSIP外呼网关
+     * 
+     * @param ids 需要删除的aiSIP外呼网关主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallGatewayByIds(Long[] ids)
+    {
+        return baseMapper.deleteAiSipCallGatewayByIds(ids);
+    }
+
+    /**
+     * 删除aiSIP外呼网关信息
+     * 
+     * @param id aiSIP外呼网关主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallGatewayById(Long id)
+    {
+        return baseMapper.deleteAiSipCallGatewayById(id);
+    }
+}

+ 108 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallLlmAgentAccountServiceImpl.java

@@ -0,0 +1,108 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallLlmAgentAccount;
+import com.fs.aiSipCall.mapper.AiSipCallLlmAgentAccountMapper;
+import com.fs.aiSipCall.service.IAiSipCallLlmAgentAccountService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼大模型Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@Slf4j
+@Service
+public class AiSipCallLlmAgentAccountServiceImpl extends ServiceImpl<AiSipCallLlmAgentAccountMapper, AiSipCallLlmAgentAccount> implements IAiSipCallLlmAgentAccountService {
+
+    /**
+     * 查询aiSIP外呼大模型
+     * 
+     * @param id aiSIP外呼大模型主键
+     * @return aiSIP外呼大模型
+     */
+    @Override
+    public AiSipCallLlmAgentAccount selectAiSipCallLlmAgentAccountById(Long id)
+    {
+        return baseMapper.selectAiSipCallLlmAgentAccountById(id);
+    }
+
+    /**
+     * 查询aiSIP外呼大模型列表
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return aiSIP外呼大模型
+     */
+    @Override
+    public List<AiSipCallLlmAgentAccount> selectAiSipCallLlmAgentAccountList(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount)
+    {
+        //先使用远程大模型
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.LLMACOUNT_LIST_API);
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                String data = jsonObject.getString("data");
+                return JSONObject.parseArray(data, AiSipCallLlmAgentAccount.class);
+            }else{
+                log.error("获取大模型接口失败:{}", jsonObject.getString("msg"));
+            }
+        }
+        return null;
+//        return baseMapper.selectAiSipCallLlmAgentAccountList(aiSipCallLlmAgentAccount);
+    }
+
+    /**
+     * 新增aiSIP外呼大模型
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return 结果
+     */
+    @Override
+    public int insertAiSipCallLlmAgentAccount(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount)
+    {
+        return baseMapper.insertAiSipCallLlmAgentAccount(aiSipCallLlmAgentAccount);
+    }
+
+    /**
+     * 修改aiSIP外呼大模型
+     * 
+     * @param aiSipCallLlmAgentAccount aiSIP外呼大模型
+     * @return 结果
+     */
+    @Override
+    public int updateAiSipCallLlmAgentAccount(AiSipCallLlmAgentAccount aiSipCallLlmAgentAccount)
+    {
+        return baseMapper.updateAiSipCallLlmAgentAccount(aiSipCallLlmAgentAccount);
+    }
+
+    /**
+     * 批量删除aiSIP外呼大模型
+     * 
+     * @param ids 需要删除的aiSIP外呼大模型主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallLlmAgentAccountByIds(Long[] ids)
+    {
+        return baseMapper.deleteAiSipCallLlmAgentAccountByIds(ids);
+    }
+
+    /**
+     * 删除aiSIP外呼大模型信息
+     * 
+     * @param id aiSIP外呼大模型主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallLlmAgentAccountById(Long id)
+    {
+        return baseMapper.deleteAiSipCallLlmAgentAccountById(id);
+    }
+}

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

@@ -0,0 +1,543 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallOutboundCdr;
+import com.fs.aiSipCall.domain.CcCustInfo;
+import com.fs.aiSipCall.mapper.AiSipCallOutboundCdrMapper;
+import com.fs.aiSipCall.param.ApiCallRecordByUuidQueryParams;
+import com.fs.aiSipCall.param.ApiCallRecordQueryParams;
+import com.fs.aiSipCall.service.IAiSipCallOutboundCdrService;
+import com.fs.aiSipCall.utils.DateUtils;
+import com.fs.aiSipCall.vo.ApiCallRecordQueryVo;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.net.URLEncoder;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+/**
+ * aiSIP手动外呼通话记录Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-19
+ */
+@Slf4j
+@Service
+public class AiSipCallOutboundCdrServiceImpl extends ServiceImpl<AiSipCallOutboundCdrMapper, AiSipCallOutboundCdr> implements IAiSipCallOutboundCdrService {
+
+    @Autowired
+    private CompanyVoiceRoboticCallLogCallphoneMapper companyVoiceRoboticCallLogCallphoneMapper;
+
+    @Override
+    public AiSipCallOutboundCdr selectAiSipCallOutboundCdrById(String id) {
+        return baseMapper.selectAiSipCallOutboundCdrById(id);
+    }
+    /**
+     * 查询aiSIP手动外呼通话记录列表
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return aiSIP手动外呼通话记录
+     */
+    @Override
+    public List<AiSipCallOutboundCdr> selectAiSipCallOutboundCdrList(AiSipCallOutboundCdr aiSipCallOutboundCdr)
+    {
+        if (aiSipCallOutboundCdr.getTimeLenStart() != null) {
+            aiSipCallOutboundCdr.setTimeLenStart(aiSipCallOutboundCdr.getTimeLenStart() * 60 * 1000L);
+        }
+        if (aiSipCallOutboundCdr.getTimeLenEnd() != null) {
+            aiSipCallOutboundCdr.setTimeLenEnd(aiSipCallOutboundCdr.getTimeLenEnd() * 60 * 1000L);
+        }
+        if (StringUtils.isNotBlank(aiSipCallOutboundCdr.getStartTimeStart())) {
+            aiSipCallOutboundCdr.setStartTimeStartLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallOutboundCdr.getStartTimeStart()).getTime());
+        }
+        if (StringUtils.isNotBlank(aiSipCallOutboundCdr.getStartTimeEnd())) {
+            aiSipCallOutboundCdr.setStartTimeEndLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallOutboundCdr.getStartTimeEnd()).getTime());
+        }
+        if (StringUtils.isNotBlank(aiSipCallOutboundCdr.getAnsweredTimeStart())) {
+            aiSipCallOutboundCdr.setAnsweredTimeStartLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallOutboundCdr.getAnsweredTimeStart()).getTime());
+        }
+        if (StringUtils.isNotBlank(aiSipCallOutboundCdr.getAnsweredTimeEnd())) {
+            aiSipCallOutboundCdr.setAnsweredTimeEndLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallOutboundCdr.getAnsweredTimeEnd()).getTime());
+        }
+
+        if (StringUtils.isNotBlank(aiSipCallOutboundCdr.getEndTimeStart())) {
+            aiSipCallOutboundCdr.setEndTimeStartLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallOutboundCdr.getEndTimeStart()).getTime());
+        }
+        if (StringUtils.isNotBlank(aiSipCallOutboundCdr.getEndTimeEnd())) {
+            aiSipCallOutboundCdr.setEndTimeEndLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallOutboundCdr.getEndTimeEnd()).getTime());
+        }
+        return baseMapper.selectAiSipCallOutboundCdrList(aiSipCallOutboundCdr);
+    }
+
+    /**
+     * 新增aiSIP手动外呼通话记录
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return 结果
+     */
+    @Override
+    public int insertAiSipCallOutboundCdr(AiSipCallOutboundCdr aiSipCallOutboundCdr)
+    {
+        return baseMapper.insertAiSipCallOutboundCdr(aiSipCallOutboundCdr);
+    }
+
+    /**
+     * 修改aiSIP手动外呼通话记录
+     * 
+     * @param aiSipCallOutboundCdr aiSIP手动外呼通话记录
+     * @return 结果
+     */
+    @Override
+    public int updateAiSipCallOutboundCdr(AiSipCallOutboundCdr aiSipCallOutboundCdr)
+    {
+        return baseMapper.updateAiSipCallOutboundCdr(aiSipCallOutboundCdr);
+    }
+
+    /**
+     * 批量删除aiSIP手动外呼通话记录
+     * 
+     * @param ids 需要删除的aiSIP手动外呼通话记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallOutboundCdrByIds(String[] ids)
+    {
+        return baseMapper.deleteAiSipCallOutboundCdrByIds(ids);
+    }
+
+    /**
+     * 删除aiSIP手动外呼通话记录信息
+     * 
+     * @param id aiSIP手动外呼通话记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallOutboundCdrById(String id)
+    {
+        return baseMapper.deleteAiSipCallOutboundCdrById(id);
+    }
+
+    @Override
+    public AjaxResult getCustCommunicationInfo(String phoneNum, Integer callType, String uuid) {
+        String paramStr = "?phoneNum=" + phoneNum + "&callType=" + callType + "&uuid=" + uuid;
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.GET_CUST_COMMUNICATION_INFO_API + paramStr);
+        if (StringUtils.isNotBlank(result)) {
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if (jsonObject.getInteger("code") == 0) {
+                return JSONObject.parseObject(result, AjaxResult.class);
+            } else {
+                log.error("取手动外呼客户沟通信息失败:{}", jsonObject.getString("msg"));
+            }
+        } else {
+            log.error("取手动外呼客户沟通信息失败:{}", "接口返回为空");
+        }
+        return AjaxResult.error();
+    }
+
+    @Override
+    public AjaxResult addCustcallrecord(CcCustInfo ccCustInfo) {
+        String result = RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.ADD_CUSTCALL_RECORD_API,JSONObject.toJSONString(ccCustInfo));
+        if (StringUtils.isNotBlank(result)) {
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if (jsonObject.getInteger("code") == 0) {
+                return JSONObject.parseObject(result, AjaxResult.class);
+            } else {
+                log.error("取手动外呼客户沟通信息失败:{}", jsonObject.getString("msg"));
+            }
+        } else {
+            log.error("取手动外呼客户沟通信息失败:{}", "接口返回为空");
+        }
+        return AjaxResult.error();
+    }
+
+    private final AtomicBoolean isRunning = new AtomicBoolean(false);
+    @Override
+    @Async
+    public CompletableFuture<String> scheduledGetCallRecord() {
+        if (!isRunning.compareAndSet(false, true)) {
+            log.warn("scheduledGetCallRecord 任务正在执行中,请稍后再试");
+            return CompletableFuture.completedFuture("任务正在执行中,请稍后再试");
+        }
+
+        try {
+            log.info("开始执行 scheduledGetCallRecord 异步任务");
+            String todayStartStr = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, new Date()) + " 00:00:00";
+            String now = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date());
+            long startTime = System.currentTimeMillis();
+
+            // 构建查询参数
+            List<ApiCallRecordQueryParams> paramsList = buildDayQueryParams(todayStartStr,now);
+
+            // 获取远程数据
+            List<AiSipCallOutboundCdr> remoteList = fetchAllRemoteCallRecords(paramsList);
+            if (remoteList.isEmpty()) {
+                log.info("scheduledGetCallRecord 异步任务完成,耗时:{}ms, 结果:当天无最新数据", System.currentTimeMillis() - startTime);
+                return CompletableFuture.completedFuture("当天无最新数据");
+            }
+
+            // 获取本地当天所有外呼记录
+            List<AiSipCallOutboundCdr> localList = baseMapper.selectCurrentDayCallRecords(DateUtils.toStartTime(), System.currentTimeMillis());
+
+            // 筛选并处理新增和更新数据
+            String result = processAndSaveData(remoteList, localList);
+            log.info("scheduledGetCallRecord 异步任务完成,耗时:{}ms, 结果:{}", System.currentTimeMillis() - startTime, result);
+            return CompletableFuture.completedFuture(result);
+        } catch (Exception e) {
+            log.error("scheduledGetCallRecord 异步任务执行失败", e);
+            return CompletableFuture.completedFuture("任务执行失败:" + e.getMessage());
+        } finally {
+            isRunning.set(false);
+        }
+    }
+
+    /**
+     * 构建当天查询参数 - 使用当天 00:00:00 到当前时间
+     * @return 查询参数对象列表(支持多时段查询)
+     */
+    private List<ApiCallRecordQueryParams> buildDayQueryParams(String todayStartStr,String now) {
+
+        List<ApiCallRecordQueryParams> paramsList = new ArrayList<>();
+
+        // 如果时间跨度超过 12 小时,分段查询避免遗漏
+        Date today = DateUtils.parseDate(todayStartStr);
+        long hoursDiff = (System.currentTimeMillis() - today.getTime()) / (1000 * 60 * 60);
+
+        if (hoursDiff > 12) {
+            // 分两段查询:00:00-12:00 和 12:00-当前时间
+            ApiCallRecordQueryParams params1 = createSingleParam(todayStartStr, todayStartStr.substring(0, 10) + " 12:00:00");
+            ApiCallRecordQueryParams params2 = createSingleParam(todayStartStr.substring(0, 10) + " 12:00:00", now);
+            paramsList.add(params1);
+            paramsList.add(params2);
+        } else {
+            // 单段查询:00:00-当前时间
+            paramsList.add(createSingleParam(todayStartStr, now));
+        }
+
+        return paramsList;
+    }
+
+    /**
+     * 创建单个查询参数对象
+     */
+    private ApiCallRecordQueryParams createSingleParam(String startTime, String endTime) {
+        ApiCallRecordQueryParams params = new ApiCallRecordQueryParams();
+        params.setPageNum(1);
+        params.setPageSize(1000); // 减小单次请求量,提升响应速度
+        params.setCallType("03");
+        params.setCalloutTimeStart(startTime);
+        params.setCalloutTimeEnd(endTime);
+        return params;
+    }
+
+    /**
+     * 分页轮询获取所有远程通话记录 (带去重和失败重试)
+     * @param paramsList 查询参数列表
+     * @return 通话记录列表 (已去重)
+     */
+    private List<AiSipCallOutboundCdr> fetchAllRemoteCallRecords(List<ApiCallRecordQueryParams> paramsList) {
+        List<AiSipCallOutboundCdr> allRecords = new ArrayList<>();
+        for (ApiCallRecordQueryParams params : paramsList) {
+            int currentPage = 1;
+            boolean hasMore = true;
+
+            while (hasMore) {
+                params.setPageNum(currentPage);
+                List<AiSipCallOutboundCdr> pageData = fetchSinglePageRecords(params);
+
+                // 失败时跳过该页,继续下一页
+                if (pageData == null) {
+                    log.warn("分页查询第{}页失败,已丢弃该页数据", currentPage);
+                    currentPage++;
+                    continue;
+                }
+
+                if (pageData.isEmpty()) {
+                    log.info("第{}页无数据,查询结束", currentPage);
+                    hasMore = false;
+                } else {
+                    allRecords.addAll(pageData);
+                    log.debug("第{}页数据:{},累计总数:{}", currentPage, pageData.size(), allRecords.size());
+
+                    // 如果返回数据少于页大小,说明已是最后一页
+                    if (pageData.size() < params.getPageSize()) {
+                        hasMore = false;
+                    }
+                    currentPage++;
+                }
+
+                // 安全限制:最多拉取 50 页
+                if (currentPage > 50) {
+                    log.warn("已达到最大页数限制 50 页,停止查询。已获取数据量:{}", allRecords.size());
+                    hasMore = false;
+                }
+            }
+        }
+
+        log.info("远程数据获取完成,总计:{} 条", allRecords.size());
+        return allRecords.isEmpty() ? Collections.emptyList() : allRecords;
+    }
+
+    /**
+     * 获取单页数据
+     * @param params 查询参数(需预先设置页码)
+     * @return 单页通话记录列表
+     */
+    private List<AiSipCallOutboundCdr> fetchSinglePageRecords(ApiCallRecordQueryParams params) {
+        try {
+            String result = RemoteCommon.sendPost(
+                    RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.CALL_RECORDS_API,
+                    JSONObject.toJSONString(params)
+            );
+
+            if (StringUtils.isBlank(result)) {
+                log.error("查询第{}页失败:接口返回为空", params.getPageNum());
+                return null;
+            }
+
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            Integer code = jsonObject.getInteger("code");
+
+            if (code == null || code != 0) {
+                String errorMsg = jsonObject.getString("msg") != null
+                        ? jsonObject.getString("msg") : "未知错误";
+                log.error("查询第{}页失败:{}", params.getPageNum(), errorMsg);
+                return null;
+            }
+
+            Long total = jsonObject.getLong("total");
+            if (total == null || total <= 0) {
+                return Collections.emptyList();
+            }
+
+            String rows = jsonObject.getString("rows");
+            if (StringUtils.isBlank(rows)) {
+                return Collections.emptyList();
+            }
+
+            return JSONObject.parseArray(rows, AiSipCallOutboundCdr.class);
+        } catch (Exception e) {
+            log.error("查询第{}页异常", params.getPageNum(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 处理并保存数据 (新增和更新)
+     * @param remoteList 远程数据列表
+     * @param localList 本地数据列表
+     * @return 处理结果信息
+     */
+    private String processAndSaveData(List<AiSipCallOutboundCdr> remoteList, List<AiSipCallOutboundCdr> localList) {
+        if (remoteList == null || remoteList.isEmpty()) {
+            return "远程数据列表为空,无需处理";
+        }
+
+        if (localList == null || localList.isEmpty()) {
+            log.warn("本地数据列表为空,所有远程数据都将作为新增处理");
+            return processDataWithStats(remoteList, "新增");
+        }
+
+        // 构建本地数据的唯一标识映射
+        Set<String> localIdSet = localList.stream()
+                .map(AiSipCallOutboundCdr::getId)
+                .collect(Collectors.toSet());
+
+        // 分类数据:新增和更新
+        List<AiSipCallOutboundCdr> insertList = new ArrayList<>();
+        List<AiSipCallOutboundCdr> updateList = new ArrayList<>();
+
+        for (AiSipCallOutboundCdr remote : remoteList) {
+            if (StringUtils.isBlank(remote.getId())) {
+                continue;
+            }
+            if (localIdSet.contains(remote.getId())) {
+                updateList.add(remote);
+            } else {
+                insertList.add(remote);
+            }
+        }
+
+        log.info("数据筛选完成 - 预计新增:{},预计更新:{}", insertList.size(), updateList.size());
+
+        StringBuilder resultMsg = new StringBuilder();
+        if (!insertList.isEmpty()) {
+            resultMsg.append(processDataWithStats(insertList, "新增"));
+        }
+
+        if (!updateList.isEmpty()) {
+            if (resultMsg.length() > 0) {
+                resultMsg.append(",");
+            }
+            resultMsg.append(processDataWithStats(updateList, "更新"));
+        }
+
+        return resultMsg.toString();
+    }
+
+    /**
+     * 处理批量数据并统计成功失败信息
+     * @param dataList 数据列表
+     * @param operationType 操作类型:"新增" 或 "更新"
+     * @return 处理结果信息
+     */
+    private String processDataWithStats(List<AiSipCallOutboundCdr> dataList,String operationType) {
+        log.info("开始处理{}Ai 外呼记录数据,数量:{}", operationType, dataList.size());
+        if (dataList.isEmpty()) {
+            return "无有效数据,无需处理";
+        }
+
+        int batchSize = 500;
+        int totalSize = dataList.size();
+        int batchCount = (totalSize + batchSize - 1) / batchSize;
+        int successCount = 0;
+        int failCount = 0;
+        List<int[]> failedBatchRanges = new ArrayList<>();
+
+        for (int i = 0; i < batchCount; i++) {
+            int fromIndex = i * batchSize;
+            int toIndex = Math.min(fromIndex + batchSize, totalSize);
+            List<AiSipCallOutboundCdr> batchList = dataList.subList(fromIndex, toIndex);
+
+            try {
+                if ("新增".equals(operationType)) {
+                    this.saveBatch(batchList);
+                } else if ("更新".equals(operationType)) {
+                    this.updateBatchById(batchList);
+                }
+                successCount += batchList.size();
+                log.debug("第{}/{}批{}成功,本批数量:{}", i + 1, batchCount, operationType, batchList.size());
+            } catch (Exception e) {
+                failCount += batchList.size();
+                int[] range = {fromIndex, toIndex - 1};
+                failedBatchRanges.add(range);
+                log.error("第{}批数据{}失败,本批数量:{},起始位:{},结束位:{}",
+                        i + 1, operationType, batchList.size(), fromIndex, toIndex - 1, e);
+            }
+        }
+
+        StringBuilder result = new StringBuilder();
+        result.append(operationType).append("数据处理完成,总计:").append(totalSize).append("条");
+        result.append(",成功:").append(successCount).append("条");
+
+        if (failCount > 0) {
+            result.append(",失败:").append(failCount).append("条");
+            result.append(",失败数据位置:");
+            for (int i = 0; i < failedBatchRanges.size(); i++) {
+                int[] range = failedBatchRanges.get(i);
+                if (i > 0) result.append(";");
+                result.append("[").append(range[0]).append("-").append(range[1]).append("]");
+            }
+        }
+
+        return result.toString();
+    }
+
+    @Override
+    public int syncByUuid(ApiCallRecordByUuidQueryParams req) {
+        if (req == null || StringUtils.isBlank(req.getUuid())) {
+//            throw new ServiceException("uuid不能为空");
+        }
+
+        String callType = StringUtils.isBlank(req.getCallType()) ? "03" : req.getCallType();
+
+        // 1. 先查本地是否已存在,防重复
+//        CompanyVoiceRoboticCallLogCallphone exist = companyVoiceRoboticCallLogCallphoneMapper.selectByCallbackUuid(req.getUuid());
+//        if (exist != null) {
+//            log.info("通话记录已存在,无需重复同步,uuid={}", req.getUuid());
+//            return 1;
+//        }
+
+        // 2. 调远程接口按 uuid 查询
+        ApiCallRecordQueryVo remoteRecord = getRemoteRecordByUuid(req.getUuid(), callType);
+        if (remoteRecord == null) {
+            log.warn("远程未查到通话记录,uuid={}, callType={}", req.getUuid(), callType);
+            return 0;
+        }
+
+        // 3. 转成本地实体
+        CompanyVoiceRoboticCallLogCallphone entity = buildLocalEntity(remoteRecord, callType,2);
+
+        // 4. 插入本地表
+        return companyVoiceRoboticCallLogCallphoneMapper.insertCompanyVoiceRoboticCallLogCallphone(entity);
+    }
+
+    private ApiCallRecordQueryVo getRemoteRecordByUuid(String uuid, String callType) {
+        try {
+            String url = RemoteCommon.REMOTE_ADDERSS_PREFIX
+                    + RemoteCommon.QUERY_OUTBOUNDCDR_BYUUID_API
+                    + "?uuid=" + URLEncoder.encode(uuid, "UTF-8")
+                    + "&callType=" + URLEncoder.encode(callType, "UTF-8");
+
+            String result = RemoteCommon.sendGet(url);
+
+            if (StringUtils.isBlank(result)) {
+                log.error("远程查询通话记录失败,返回为空,uuid={}", uuid);
+                return null;
+            }
+
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            Integer code = jsonObject.getInteger("code");
+            if (code == null || code != 0) {
+                String msg = jsonObject.getString("msg");
+                log.error("远程查询通话记录失败,uuid={}, msg={}", uuid, msg);
+                return null;
+            }
+
+            Object dataObj = jsonObject.get("data");
+            if (dataObj == null) {
+                return null;
+            }
+
+            return JSONObject.parseObject(JSONObject.toJSONString(dataObj), ApiCallRecordQueryVo.class);
+        } catch (Exception e) {
+            log.error("远程查询通话记录异常,uuid={}", uuid, e);
+            return null;
+        }
+    }
+
+
+    private CompanyVoiceRoboticCallLogCallphone buildLocalEntity(ApiCallRecordQueryVo remoteRecord, String callType,Integer status) {
+        CompanyVoiceRoboticCallLogCallphone entity = new CompanyVoiceRoboticCallLogCallphone();
+
+        entity.setCallbackUuid(null);
+        entity.setRoboticId(12345L);
+        entity.setCallerId(null);
+        entity.setRunTime(null);
+        entity.setRunParam(null);
+        entity.setResult(null);
+        entity.setStatus(status);
+        entity.setRecordPath(remoteRecord.getWavFileUrl());
+        entity.setCallerNum(remoteRecord.getTelephone());
+        entity.setCalleeNum(remoteRecord.getCallerNumber());
+        entity.setUuid(remoteRecord.getUuid());
+//        entity.setCallCreateTime(Long.valueOf(remoteRecord.getManualAnsweredTime()));
+//        entity.setCallAnswerTime(Long.valueOf(remoteRecord.getAnsweredTime()));
+        entity.setIntention(null);
+        entity.setCompanyId(null);
+        entity.setCompanyUserId(null);
+        entity.setCallTime(Long.valueOf(remoteRecord.getTimeLen()));
+        entity.setCost(null);
+        entity.setCallType(Integer.valueOf(callType));
+        if (remoteRecord.getDialogue() != null) {
+            entity.setContentList(JSONObject.toJSONString(remoteRecord.getDialogue()));
+        }
+
+        entity.setCreateTime(new Date());
+        entity.setUpdateTime(new Date());
+
+        return entity;
+    }
+
+
+}

+ 429 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallPhoneServiceImpl.java

@@ -0,0 +1,429 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallPhone;
+import com.fs.aiSipCall.dto.CallTaskStatModel;
+import com.fs.aiSipCall.dto.CommonPhoneModel;
+import com.fs.aiSipCall.mapper.AiSipCallPhoneMapper;
+import com.fs.aiSipCall.param.ApiCallRecordQueryParams;
+import com.fs.aiSipCall.service.IAiSipCallPhoneService;
+import com.fs.aiSipCall.utils.DateUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+/**
+ * aiSIP外呼通话记录Service业务层处理
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Slf4j
+@Service
+public class AiSipCallPhoneServiceImpl extends ServiceImpl<AiSipCallPhoneMapper, AiSipCallPhone> implements IAiSipCallPhoneService {
+
+    /**
+     * 查询aiSIP外呼通话记录
+     *
+     * @param id aiSIP外呼通话记录主键
+     * @return aiSIP外呼通话记录
+     */
+    @Override
+    public AiSipCallPhone selectAiSipCallPhoneById(String id) {
+        return baseMapper.selectAiSipCallPhoneById(id);
+    }
+
+    /**
+     * 查询aiSIP外呼通话记录列表
+     *
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return aiSIP外呼通话记录
+     */
+    @Override
+    public List<AiSipCallPhone> selectAiSipCallPhoneList(AiSipCallPhone aiSipCallPhone) {
+
+        if (aiSipCallPhone.getTimeLenStart() != null) {
+            aiSipCallPhone.setTimeLenStart(aiSipCallPhone.getTimeLenStart() * 60 * 1000L);
+        }
+        if (aiSipCallPhone.getTimeLenEnd() != null) {
+            aiSipCallPhone.setTimeLenEnd(aiSipCallPhone.getTimeLenEnd() * 60 * 1000L);
+        }
+        if (StringUtils.isNotBlank(aiSipCallPhone.getCalloutTimeStart())) {
+            aiSipCallPhone.setCalloutTimeStartLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallPhone.getCalloutTimeStart()).getTime());
+        }
+        if (StringUtils.isNotBlank(aiSipCallPhone.getCalloutTimeEnd())) {
+            aiSipCallPhone.setCalloutTimeEndLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallPhone.getCalloutTimeEnd()).getTime());
+        }
+        if (StringUtils.isNotBlank(aiSipCallPhone.getAnsweredTimeStart())) {
+            aiSipCallPhone.setAnsweredTimeStartLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallPhone.getAnsweredTimeStart()).getTime());
+        }
+        if (StringUtils.isNotBlank(aiSipCallPhone.getAnsweredTimeEnd())) {
+            aiSipCallPhone.setAnsweredTimeEndLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallPhone.getAnsweredTimeEnd()).getTime());
+        }
+
+        if (StringUtils.isNotBlank(aiSipCallPhone.getCallEndTimeStart())) {
+            aiSipCallPhone.setCallEndTimeStartLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallPhone.getCallEndTimeStart()).getTime());
+        }
+        if (StringUtils.isNotBlank(aiSipCallPhone.getCallEndTimeEnd())) {
+            aiSipCallPhone.setCallEndTimeEndLong(DateUtils.dateTime("yyyy-MM-dd HH:mm:ss", aiSipCallPhone.getCallEndTimeEnd()).getTime());
+        }
+        return baseMapper.selectAiSipCallPhoneList(aiSipCallPhone);
+    }
+
+    /**
+     * 新增aiSIP外呼通话记录
+     *
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return 结果
+     */
+    @Override
+    public int insertAiSipCallPhone(AiSipCallPhone aiSipCallPhone) {
+        return baseMapper.insertAiSipCallPhone(aiSipCallPhone);
+    }
+
+    /**
+     * 修改aiSIP外呼通话记录
+     *
+     * @param aiSipCallPhone aiSIP外呼通话记录
+     * @return 结果
+     */
+    @Override
+    public int updateAiSipCallPhone(AiSipCallPhone aiSipCallPhone) {
+        return baseMapper.updateAiSipCallPhone(aiSipCallPhone);
+    }
+
+    /**
+     * 批量删除aiSIP外呼通话记录
+     *
+     * @param ids 需要删除的aiSIP外呼通话记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallPhoneByIds(String[] ids) {
+        return baseMapper.deleteAiSipCallPhoneByIds(ids);
+    }
+
+    /**
+     * 删除aiSIP外呼通话记录信息
+     *
+     * @param id aiSIP外呼通话记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallPhoneById(String id) {
+        return baseMapper.deleteAiSipCallPhoneById(id);
+    }
+
+    @Override
+    public CallTaskStatModel statByBatchId(Long batchId) {
+        CallTaskStatModel callTaskStatModel = baseMapper.statByBatchId(batchId);
+        if (null == callTaskStatModel.getPhoneCount()) {
+            callTaskStatModel.setPhoneCount(0);
+        }
+        if (null == callTaskStatModel.getCallCount()) {
+            callTaskStatModel.setCallCount(0);
+        }
+        if (null == callTaskStatModel.getConnectCount()) {
+            callTaskStatModel.setConnectCount(0);
+        }
+        return callTaskStatModel;
+    }
+
+    @Override
+    public List<String> isDuplicateEntry(Long batchId, List<CommonPhoneModel> phoneList) {
+        return baseMapper.isDuplicateEntry(batchId, phoneList);
+    }
+
+    private final AtomicBoolean isRunning = new AtomicBoolean(false);
+
+    @Override
+    @Async
+    public CompletableFuture<String> scheduledGetCallRecord() {
+        if (!isRunning.compareAndSet(false, true)) {
+            log.warn("scheduledGetCallRecord 任务正在执行中,请稍后再试");
+            return CompletableFuture.completedFuture("任务正在执行中,请稍后再试");
+        }
+
+        try {
+            log.info("开始执行 scheduledGetCallRecord 异步任务");
+            String todayStartStr = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, new Date()) + " 00:00:00";
+            String now = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date());
+            long startTime = System.currentTimeMillis();
+
+            // 构建查询参数
+            List<ApiCallRecordQueryParams> paramsList = buildDayQueryParams(todayStartStr,now);
+
+            // 获取远程数据
+            List<AiSipCallPhone> remoteList = fetchAllRemoteCallRecords(paramsList);
+            if (remoteList.isEmpty()) {
+                log.info("scheduledGetCallRecord 异步任务完成,耗时:{}ms, 结果:当天无最新数据", System.currentTimeMillis() - startTime);
+                return CompletableFuture.completedFuture("当天无最新数据");
+            }
+
+            // 获取本地当天所有外呼记录
+            List<AiSipCallPhone> localList = baseMapper.selectCurrentDayCallRecords(DateUtils.toStartTime(), System.currentTimeMillis());
+
+            // 筛选并处理新增和更新数据
+            String result = processAndSaveData(remoteList, localList);
+            log.info("scheduledGetCallRecord 异步任务完成,耗时:{}ms, 结果:{}", System.currentTimeMillis() - startTime, result);
+            return CompletableFuture.completedFuture(result);
+        } catch (Exception e) {
+            log.error("scheduledGetCallRecord 异步任务执行失败", e);
+            return CompletableFuture.completedFuture("任务执行失败:" + e.getMessage());
+        } finally {
+            isRunning.set(false);
+        }
+    }
+        
+    /**
+     * 构建当天查询参数 - 使用当天 00:00:00 到当前时间
+     * @return 查询参数对象列表(支持多时段查询)
+     */
+    private List<ApiCallRecordQueryParams> buildDayQueryParams(String todayStartStr,String now) {
+        
+        List<ApiCallRecordQueryParams> paramsList = new ArrayList<>();
+        
+        // 如果时间跨度超过 12 小时,分段查询避免遗漏
+        Date today = DateUtils.parseDate(todayStartStr);
+        long hoursDiff = (System.currentTimeMillis() - today.getTime()) / (1000 * 60 * 60);
+        
+        if (hoursDiff > 12) {
+            // 分两段查询:00:00-12:00 和 12:00-当前时间
+            ApiCallRecordQueryParams params1 = createSingleParam(todayStartStr, todayStartStr.substring(0, 10) + " 12:00:00");
+            ApiCallRecordQueryParams params2 = createSingleParam(todayStartStr.substring(0, 10) + " 12:00:00", now);
+            paramsList.add(params1);
+            paramsList.add(params2);
+        } else {
+            // 单段查询:00:00-当前时间
+            paramsList.add(createSingleParam(todayStartStr, now));
+        }
+        
+        return paramsList;
+    }
+    
+    /**
+     * 创建单个查询参数对象
+     */
+    private ApiCallRecordQueryParams createSingleParam(String startTime, String endTime) {
+        ApiCallRecordQueryParams params = new ApiCallRecordQueryParams();
+        params.setPageNum(1);
+        params.setPageSize(1000); // 减小单次请求量,提升响应速度
+        params.setCallType("02");
+        params.setCalloutTimeStart(startTime);
+        params.setCalloutTimeEnd(endTime);
+        return params;
+    }
+        
+    /**
+     * 分页轮询获取所有远程通话记录 (带去重和失败重试)
+     * @param paramsList 查询参数列表
+     * @return 通话记录列表 (已去重)
+     */
+    private List<AiSipCallPhone> fetchAllRemoteCallRecords(List<ApiCallRecordQueryParams> paramsList) {
+        List<AiSipCallPhone> allRecords = new ArrayList<>();
+        for (ApiCallRecordQueryParams params : paramsList) {
+            int currentPage = 1;
+            boolean hasMore = true;
+                
+            while (hasMore) {
+                params.setPageNum(currentPage);
+                List<AiSipCallPhone> pageData = fetchSinglePageRecords(params);
+                    
+                // 失败时跳过该页,继续下一页
+                if (pageData == null) {
+                    log.warn("分页查询第{}页失败,已丢弃该页数据", currentPage);
+                    currentPage++;
+                    continue;
+                }
+                    
+                if (pageData.isEmpty()) {
+                    log.info("第{}页无数据,查询结束", currentPage);
+                    hasMore = false;
+                } else {
+                    allRecords.addAll(pageData);
+                    log.debug("第{}页数据:{},累计总数:{}", currentPage, pageData.size(), allRecords.size());
+                        
+                    // 如果返回数据少于页大小,说明已是最后一页
+                    if (pageData.size() < params.getPageSize()) {
+                        hasMore = false;
+                    }
+                    currentPage++;
+                }
+                    
+                // 安全限制:最多拉取 50 页
+                if (currentPage > 50) {
+                    log.warn("已达到最大页数限制 50 页,停止查询。已获取数据量:{}", allRecords.size());
+                    hasMore = false;
+                }
+            }
+        }
+            
+        log.info("远程数据获取完成,总计:{} 条", allRecords.size());
+        return allRecords.isEmpty() ? Collections.emptyList() : allRecords;
+    }
+    
+    /**
+     * 获取单页数据
+     * @param params 查询参数(需预先设置页码)
+     * @return 单页通话记录列表
+     */
+    private List<AiSipCallPhone> fetchSinglePageRecords(ApiCallRecordQueryParams params) {
+        try {
+            String result = RemoteCommon.sendPost(
+                RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.CALL_RECORDS_API, 
+                JSONObject.toJSONString(params)
+            );
+                
+            if (StringUtils.isBlank(result)) {
+                log.error("查询第{}页失败:接口返回为空", params.getPageNum());
+                return null;
+            }
+                
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            Integer code = jsonObject.getInteger("code");
+                
+            if (code == null || code != 0) {
+                String errorMsg = jsonObject.getString("msg") != null
+                    ? jsonObject.getString("msg") : "未知错误";
+                log.error("查询第{}页失败:{}", params.getPageNum(), errorMsg);
+                return null;
+            }
+                
+            Long total = jsonObject.getLong("total");
+            if (total == null || total <= 0) {
+                return Collections.emptyList();
+            }
+
+            String rows = jsonObject.getString("rows");
+            if (StringUtils.isBlank(rows)) {
+                return Collections.emptyList();
+            }
+
+            return JSONObject.parseArray(rows, AiSipCallPhone.class);
+        } catch (Exception e) {
+            log.error("查询第{}页异常", params.getPageNum(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 处理并保存数据 (新增和更新)
+     * @param remoteList 远程数据列表
+     * @param localList 本地数据列表
+     * @return 处理结果信息
+     */
+    private String processAndSaveData(List<AiSipCallPhone> remoteList, List<AiSipCallPhone> localList) {
+        if (remoteList == null || remoteList.isEmpty()) {
+            return "远程数据列表为空,无需处理";
+        }
+    
+        if (localList == null || localList.isEmpty()) {
+            log.warn("本地数据列表为空,所有远程数据都将作为新增处理");
+            return processDataWithStats(remoteList, "新增");
+        }
+
+        // 构建本地数据的唯一标识映射
+        Set<String> localIdSet = localList.stream()
+                .map(AiSipCallPhone::getId)
+                .collect(Collectors.toSet());
+            
+        // 分类数据:新增和更新
+        List<AiSipCallPhone> insertList = new ArrayList<>();
+        List<AiSipCallPhone> updateList = new ArrayList<>();
+            
+        for (AiSipCallPhone remote : remoteList) {
+            if (StringUtils.isBlank(remote.getId())) {
+                continue;
+            }
+            if (localIdSet.contains(remote.getId())) {
+                updateList.add(remote);
+            } else {
+                insertList.add(remote);
+            }
+        }
+
+        log.info("数据筛选完成 - 预计新增:{},预计更新:{}", insertList.size(), updateList.size());
+    
+        StringBuilder resultMsg = new StringBuilder();
+        if (!insertList.isEmpty()) {
+            resultMsg.append(processDataWithStats(insertList, "新增"));
+        }
+            
+        if (!updateList.isEmpty()) {
+            if (resultMsg.length() > 0) {
+                resultMsg.append(",");
+            }
+            resultMsg.append(processDataWithStats(updateList, "更新"));
+        }
+            
+        return resultMsg.toString();
+    }
+
+    /**
+     * 处理批量数据并统计成功失败信息
+     * @param dataList 数据列表
+     * @param operationType 操作类型:"新增" 或 "更新"
+     * @return 处理结果信息
+     */
+    private String processDataWithStats(List<AiSipCallPhone> dataList,String operationType) {
+        log.info("开始处理{}Ai 外呼记录数据,数量:{}", operationType, dataList.size());
+        if (dataList.isEmpty()) {
+            return "无有效数据,无需处理";
+        }
+    
+        int batchSize = 500;
+        int totalSize = dataList.size();
+        int batchCount = (totalSize + batchSize - 1) / batchSize;
+        int successCount = 0;
+        int failCount = 0;
+        List<int[]> failedBatchRanges = new ArrayList<>();
+    
+        for (int i = 0; i < batchCount; i++) {
+            int fromIndex = i * batchSize;
+            int toIndex = Math.min(fromIndex + batchSize, totalSize);
+            List<AiSipCallPhone> batchList = dataList.subList(fromIndex, toIndex);
+    
+            try {
+                if ("新增".equals(operationType)) {
+                    this.saveBatch(batchList);
+                } else if ("更新".equals(operationType)) {
+                    this.updateBatchById(batchList);
+                }
+                successCount += batchList.size();
+                log.debug("第{}/{}批{}成功,本批数量:{}", i + 1, batchCount, operationType, batchList.size());
+            } catch (Exception e) {
+                failCount += batchList.size();
+                int[] range = {fromIndex, toIndex - 1};
+                failedBatchRanges.add(range);
+                log.error("第{}批数据{}失败,本批数量:{},起始位:{},结束位:{}", 
+                         i + 1, operationType, batchList.size(), fromIndex, toIndex - 1, e);
+            }
+        }
+    
+        StringBuilder result = new StringBuilder();
+        result.append(operationType).append("数据处理完成,总计:").append(totalSize).append("条");
+        result.append(",成功:").append(successCount).append("条");
+            
+        if (failCount > 0) {
+            result.append(",失败:").append(failCount).append("条");
+            result.append(",失败数据位置:");
+            for (int i = 0; i < failedBatchRanges.size(); i++) {
+                int[] range = failedBatchRanges.get(i);
+                if (i > 0) result.append(";");
+                result.append("[").append(range[0]).append("-").append(range[1]).append("]");
+            }
+        }
+            
+        return result.toString();
+    }
+
+
+}

+ 278 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallTaskServiceImpl.java

@@ -0,0 +1,278 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallTask;
+import com.fs.aiSipCall.dto.CallTaskStatModel;
+import com.fs.aiSipCall.dto.CommonCallListModel;
+import com.fs.aiSipCall.dto.CommonPhoneModel;
+import com.fs.aiSipCall.mapper.AiSipCallTaskMapper;
+import com.fs.aiSipCall.service.IAiSipCallPhoneService;
+import com.fs.aiSipCall.service.IAiSipCallTaskService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * aiSIP外呼任务Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@Slf4j
+@Service
+public class AiSipCallTaskServiceImpl extends ServiceImpl<AiSipCallTaskMapper, AiSipCallTask> implements IAiSipCallTaskService {
+
+
+    @Autowired
+    private IAiSipCallPhoneService aiSipCallPhoneService;
+
+    /**
+     * 查询aiSIP外呼任务
+     * 
+     * @param batchId aiSIP外呼任务主键
+     * @return aiSIP外呼任务
+     */
+    @Override
+    public AiSipCallTask selectAiSipCallTaskByBatchId(Long batchId)
+    {
+        return baseMapper.selectAiSipCallTaskByBatchId(batchId);
+    }
+
+    /**
+     * 查询aiSIP外呼任务列表
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return aiSIP外呼任务
+     */
+    @Override
+    public List<AiSipCallTask> selectAiSipCallTaskList(AiSipCallTask aiSipCallTask)
+    {
+        return baseMapper.selectAiSipCallTaskList(aiSipCallTask);
+    }
+
+    /**
+     * 新增aiSIP外呼任务
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return 结果
+     */
+    @Override
+    public int insertAiSipCallTask(AiSipCallTask aiSipCallTask)
+    {
+        //同步新增远程接口
+        String result = RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.CREATE_TASK_API, JSONObject.toJSONString(aiSipCallTask));
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                String data = jsonObject.getString("data");
+                JSONObject jsonObjectAiSipCallTask = JSONObject.parseObject(data);
+                Long batchId = jsonObjectAiSipCallTask.getLong("batchId");
+                aiSipCallTask.setRemoteBatchId(batchId);
+                //特殊处理字段
+                processField(aiSipCallTask);
+                return baseMapper.insertAiSipCallTask(aiSipCallTask);
+            }else{
+                log.error("新增aiSIP外呼任务失败:{}", jsonObject.getString("msg"));
+            }
+        }else{
+            log.error("新增aiSIP外呼任务失败:{}", "接口返回为空");
+        }
+        return 0;
+    }
+
+    /**
+     * 修改aiSIP外呼任务
+     * 
+     * @param aiSipCallTask aiSIP外呼任务
+     * @return 结果
+     */
+    @Override
+    public int updateAiSipCallTask(AiSipCallTask aiSipCallTask)
+    {
+        AiSipCallTask remoteTask = new AiSipCallTask();
+        BeanUtils.copyProperties(aiSipCallTask,remoteTask);
+        remoteTask.setBatchId(aiSipCallTask.getRemoteBatchId());
+        //同步修改远程接口
+        String result = RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.EDIT_TASK_API, JSONObject.toJSONString(remoteTask));
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                //特殊处理字段
+                processField(aiSipCallTask);
+                return baseMapper.updateAiSipCallTask(aiSipCallTask);
+            }else{
+                log.error("新增aiSIP外呼任务失败:{}", jsonObject.getString("msg"));
+            }
+        }else{
+            log.error("新增aiSIP外呼任务失败:{}", "接口返回为空");
+        }
+        return 0;
+    }
+
+    /**
+     * 处理字段
+     * @param aiSipCallTask 字段
+     */
+    private void processField(AiSipCallTask aiSipCallTask) {
+        if(aiSipCallTask.getAiTransferType().equals("extension")){
+            aiSipCallTask.setAiTransferData(aiSipCallTask.getAiTransferExtNumber());
+        }else if(aiSipCallTask.getAiTransferType().equals("gateway")){
+            Map<String,String> map = new HashMap<>();
+            map.put("destNumber",aiSipCallTask.getAiTransferGatewayDestNumber());
+            map.put("gatewayId",aiSipCallTask.getAiTransferGatewayId());
+            aiSipCallTask.setAiTransferData(JSONObject.toJSONString(map));
+        }else{
+            aiSipCallTask.setAiTransferData(aiSipCallTask.getAiTransferGroupId());
+        }
+    }
+
+    /**
+     * 批量删除aiSIP外呼任务
+     * 
+     * @param batchIds 需要删除的aiSIP外呼任务主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallTaskByBatchIds(Long[] batchIds)
+    {
+        return baseMapper.deleteAiSipCallTaskByBatchIds(batchIds);
+    }
+
+    /**
+     * 删除aiSIP外呼任务信息
+     * 
+     * @param batchId aiSIP外呼任务主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallTaskByBatchId(Long batchId)
+    {
+        return baseMapper.deleteAiSipCallTaskByBatchId(batchId);
+    }
+
+    @Override
+    public int startTask(Long batchId) {
+        AiSipCallTask ccCallTask = baseMapper.selectAiSipCallTaskByBatchId(batchId);
+        if(null == ccCallTask){
+            return 0;
+        }
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.START_TASK_API + "?batchId=" + ccCallTask.getRemoteBatchId());
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                // 启动成功
+                ccCallTask.setIfcall(1L);
+                ccCallTask.setExecuting(0L);
+                ccCallTask.setStopTime(0L);
+                baseMapper.updateAiSipCallTask(ccCallTask);
+                return 1;
+            }else{
+                log.error("启动外呼失败:{}", jsonObject.getString("msg"));
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public int stopTask(Long batchId) {
+        AiSipCallTask ccCallTask = baseMapper.selectAiSipCallTaskByBatchId(batchId);
+        if(null == ccCallTask){
+            return 0;
+        }
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.STOP_TASK_API + "?batchId=" + ccCallTask.getRemoteBatchId());
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                // 停止成功
+                ccCallTask.setIfcall(0L);
+                ccCallTask.setExecuting(0L);
+                ccCallTask.setStopTime(System.currentTimeMillis());
+                baseMapper.updateAiSipCallTask(ccCallTask);
+                return 1;
+            }else{
+                log.error("停止外呼失败:{}", jsonObject.getString("msg"));
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public int commonImportExcel(Long batchId,List<CommonPhoneModel> phoneList) {
+        if (batchId == null) {
+            throw new RuntimeException("参数 batchId 不能为空");
+        }
+
+        AiSipCallTask task = this.selectAiSipCallTaskByBatchId(batchId);
+        if (task == null) {
+            throw new RuntimeException("该任务不存在,请输入正确的 batchId");
+        }
+
+        if (CollectionUtils.isEmpty(phoneList)) {
+            throw new RuntimeException("导入的数据不能为空");
+        }
+
+//        // 验证并过滤重复数据(丢到外呼那边去处理)
+//        List<String> duplicateList = aiSipCallPhoneService.isDuplicateEntry(batchId, phoneList);
+//        if (!CollectionUtils.isEmpty(duplicateList)) {
+//            phoneList = phoneList.stream()
+//                    .filter(p -> !duplicateList.contains(p.getPhoneNum()))
+//                    .collect(Collectors.toList());
+//        }
+//
+//        if (CollectionUtils.isEmpty(phoneList)) {
+//            throw new RuntimeException("过滤重复数据后无有效数据,重复号码:" + String.join(",", duplicateList));
+//        }
+        CommonCallListModel commonCallListModel = new CommonCallListModel();
+        commonCallListModel.setBatchId(task.getRemoteBatchId());
+        commonCallListModel.setPhoneList(phoneList);
+        String result = RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.COMMON_ADD_CALL_LIST_API, JSONObject.toJSONString(commonCallListModel));
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                // 追加名单成功 是先创建外呼记录还是直接取拉取完整数据
+//                int successCount = 0;
+//                List <AiSipCallPhone> callPhoneList = new ArrayList<>();
+//                for (CommonPhoneModel commonPhoneModel : commonCallListModel.getPhoneList()) {
+//                    if (StringUtils.isBlank(commonPhoneModel.getPhoneNum())) {
+//                        continue;
+//                    }
+//                    AiSipCallPhone callPhone = buildCcCallPhone(batchId, commonPhoneModel.getPhoneNum(), commonPhoneModel.getBizJson());
+//                    callPhone.setTtsText(commonPhoneModel.getNoticeContent());
+//                    callPhoneList.add(callPhone);
+//                    successCount ++;
+//                    if (callPhoneList.size() >= 200) {
+//                        aiSipCallPhoneService.saveBatch(callPhoneList);
+//                        callPhoneList = new ArrayList<>();
+//                    }
+//                }
+//                if (!callPhoneList.isEmpty()) {
+//                    aiSipCallPhoneService.saveBatch(callPhoneList);
+//                }
+//                log.info("成功追加" + successCount + "个名单");
+                return 1;
+            }else{
+                log.error("任务:{}添加名单失败:{}",batchId, jsonObject.getString("msg"));
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public CallTaskStatModel statByBatchId(Long batchId) {
+        return aiSipCallPhoneService.statByBatchId(batchId);
+    }
+
+    @Override
+    public AiSipCallTask selectAiSipCallTaskByRemoteBatchId(Long remoteBatchId) {
+        return baseMapper.selectAiSipCallTaskByRemoteBatchId(remoteBatchId);
+    }
+}

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

@@ -0,0 +1,183 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+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.common.core.domain.AjaxResult;
+import com.fs.company.mapper.CompanyUserMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * sip用户信息Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-13
+ */
+@Slf4j
+@Service
+public class AiSipCallUserServiceImpl extends ServiceImpl<AiSipCallUserMapper, AiSipCallUser> implements IAiSipCallUserService {
+
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
+
+    /**
+     * 查询sip用户信息
+     * 
+     * @param userId sip用户信息主键
+     * @return sip用户信息
+     */
+    @Override
+    public AiSipCallUser selectAiSipCallUserByUserId(Long userId)
+    {
+        return baseMapper.selectAiSipCallUserByUserId(userId);
+    }
+
+    /**
+     * 查询sip用户信息列表
+     * 
+     * @param aiSipCallUser sip用户信息
+     * @return sip用户信息
+     */
+    @Override
+    public List<AiSipCallUser> selectAiSipCallUserList(AiSipCallUser aiSipCallUser)
+    {
+        return baseMapper.selectAiSipCallUserList(aiSipCallUser);
+    }
+
+    /**
+     * 新增sip用户信息
+     * 
+     * @param aiSipCallUser sip用户信息
+     * @return 结果
+     */
+    @Override
+    public int insertAiSipCallUser(AiSipCallUser aiSipCallUser)
+    {
+        //同步新增远程接口
+        String result = RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.ADD_USER_OR_BIND_EXTNUM_API, JSONObject.toJSONString(aiSipCallUser));
+
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                String data = jsonObject.getString("data");
+                AiSipCallUser remoteAiSipCallUser = JSONObject.parseObject(data, AiSipCallUser.class);
+                if(remoteAiSipCallUser != null){
+                    //新增
+                    aiSipCallUser.setUserId(remoteAiSipCallUser.getUserId());
+                    if(StringUtils.isNotBlank(remoteAiSipCallUser.getPhonenumber())){
+                        aiSipCallUser.setPhonenumber(remoteAiSipCallUser.getPhonenumber());
+                    }
+                    if(StringUtils.isNotBlank(remoteAiSipCallUser.getPassword())){
+                        aiSipCallUser.setPassword(remoteAiSipCallUser.getPassword());
+                    }
+                    if(remoteAiSipCallUser.getCreateTime() != null){
+                        aiSipCallUser.setCreateTime(remoteAiSipCallUser.getCreateTime());
+                    }
+                    int i = baseMapper.insertAiSipCallUser(aiSipCallUser);
+                    if( i> 0){
+                        //绑定companyUser的aiSIP外呼用户
+                        return companyUserMapper.updateCompanyUserByAiSipCall(aiSipCallUser.getCompanyUserId(),remoteAiSipCallUser.getUserId());
+                    }else{
+                        log.error("绑定aiSIP外呼用户失败");
+                    }
+                }else{
+                    log.error("新增时解析aiSIP外呼用户数据为空");
+                }
+            }else{
+                log.error("新增aiSIP外呼任务失败:{}", jsonObject.getString("msg"));
+            }
+        }else{
+            log.error("新增aiSIP外呼任务失败:{}", "接口返回为空");
+        }
+        return 0;
+    }
+
+    /**
+     * 修改sip用户信息
+     * 
+     * @param aiSipCallUser sip用户信息
+     * @return 结果
+     */
+    @Override
+    public int updateAiSipCallUser(AiSipCallUser aiSipCallUser)
+    {
+        //同步修改远程接口
+        String result = RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.EDIT_USER_OR_UNBING_EXTNUM_API, JSONObject.toJSONString(aiSipCallUser));
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                return baseMapper.updateAiSipCallUser(aiSipCallUser);
+            }else{
+                log.error("修改aiSIP外呼任务失败:{}", jsonObject.getString("msg"));
+            }
+        }else{
+            log.error("修改aiSIP外呼任务失败:{}", "接口返回为空");
+        }
+        return 0;
+    }
+
+    /**
+     * 批量删除sip用户信息
+     * 
+     * @param userIds 需要删除的sip用户信息主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallUserByUserIds(Long[] userIds)
+    {
+        return baseMapper.deleteAiSipCallUserByUserIds(userIds);
+    }
+
+    /**
+     * 删除sip用户信息信息
+     * 
+     * @param userId sip用户信息主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallUserByUserId(Long userId)
+    {
+        return baseMapper.deleteAiSipCallUserByUserId(userId);
+    }
+
+    @Override
+    public AjaxResult getUnBindExtnum() {
+        //获取未使用的分机号远程接口
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.QUERY_UN_BIND_EXTNUM_API);
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                return JSONObject.parseObject(result, AjaxResult.class);
+            }else{
+                log.error("查询aiSIP外呼未绑定分机失败:{}", jsonObject.getString("msg"));
+            }
+        }else{
+            log.error("查询aiSIP外呼未绑定分机失败:{}", "接口返回为空");
+        }
+        return AjaxResult.error();
+    }
+
+    @Override
+    public AjaxResult getToolbarBasicParam(String extNum) {
+        //先使用远程网关
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.PHONEBAR_PARAMS_API + "?extNum=" + extNum);
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                return JSONObject.parseObject(result, AjaxResult.class);
+            }else{
+                log.error("获取电话工具条的网关列表接口失败:{}", jsonObject.getString("msg"));
+            }
+        }
+        return AjaxResult.error();
+    }
+
+}

+ 108 - 0
fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallVoiceTtsAliyunServiceImpl.java

@@ -0,0 +1,108 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.aiSipCall.RemoteCommon;
+import com.fs.aiSipCall.domain.AiSipCallVoiceTtsAliyun;
+import com.fs.aiSipCall.mapper.AiSipCallVoiceTtsAliyunMapper;
+import com.fs.aiSipCall.service.IAiSipCallVoiceTtsAliyunService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * aiSIP外呼阿里云音色Service业务层处理
+ * 
+ * @author fs
+ * @date 2026-03-06
+ */
+@Slf4j
+@Service
+public class AiSipCallVoiceTtsAliyunServiceImpl extends ServiceImpl<AiSipCallVoiceTtsAliyunMapper, AiSipCallVoiceTtsAliyun> implements IAiSipCallVoiceTtsAliyunService {
+
+    /**
+     * 查询aiSIP外呼阿里云音色
+     * 
+     * @param id aiSIP外呼阿里云音色主键
+     * @return aiSIP外呼阿里云音色
+     */
+    @Override
+    public AiSipCallVoiceTtsAliyun selectAiSipCallVoiceTtsAliyunById(Long id)
+    {
+        return baseMapper.selectAiSipCallVoiceTtsAliyunById(id);
+    }
+
+    /**
+     * 查询aiSIP外呼阿里云音色列表
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return aiSIP外呼阿里云音色
+     */
+    @Override
+    public List<AiSipCallVoiceTtsAliyun> selectAiSipCallVoiceTtsAliyunList(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun)
+    {
+        //先使用远程网关
+        String result = RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.VOICECODE_LIST_API);
+        if(StringUtils.isNotBlank(result)){
+            JSONObject jsonObject = JSONObject.parseObject(result);
+            if(jsonObject.getInteger("code") == 0){
+                String data = jsonObject.getString("data");
+                return JSONObject.parseArray(data, AiSipCallVoiceTtsAliyun.class);
+            }else{
+                log.error("获取音色接口失败:{}", jsonObject.getString("msg"));
+            }
+        }
+        return null;
+//        return baseMapper.selectAiSipCallVoiceTtsAliyunList(aiSipCallVoiceTtsAliyun);
+    }
+
+    /**
+     * 新增aiSIP外呼阿里云音色
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return 结果
+     */
+    @Override
+    public int insertAiSipCallVoiceTtsAliyun(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun)
+    {
+        return baseMapper.insertAiSipCallVoiceTtsAliyun(aiSipCallVoiceTtsAliyun);
+    }
+
+    /**
+     * 修改aiSIP外呼阿里云音色
+     * 
+     * @param aiSipCallVoiceTtsAliyun aiSIP外呼阿里云音色
+     * @return 结果
+     */
+    @Override
+    public int updateAiSipCallVoiceTtsAliyun(AiSipCallVoiceTtsAliyun aiSipCallVoiceTtsAliyun)
+    {
+        return baseMapper.updateAiSipCallVoiceTtsAliyun(aiSipCallVoiceTtsAliyun);
+    }
+
+    /**
+     * 批量删除aiSIP外呼阿里云音色
+     * 
+     * @param ids 需要删除的aiSIP外呼阿里云音色主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallVoiceTtsAliyunByIds(Long[] ids)
+    {
+        return baseMapper.deleteAiSipCallVoiceTtsAliyunByIds(ids);
+    }
+
+    /**
+     * 删除aiSIP外呼阿里云音色信息
+     * 
+     * @param id aiSIP外呼阿里云音色主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAiSipCallVoiceTtsAliyunById(Long id)
+    {
+        return baseMapper.deleteAiSipCallVoiceTtsAliyunById(id);
+    }
+}

+ 85 - 0
fs-service/src/main/java/com/fs/aiSipCall/utils/CharsetKit.java

@@ -0,0 +1,85 @@
+package com.fs.aiSipCall.utils;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 字符集工具类
+ * 
+ * @author ruoyi
+ */
+public class CharsetKit
+{
+    /** ISO-8859-1 */
+    public static final String ISO_8859_1 = "ISO-8859-1";
+    /** UTF-8 */
+    public static final String UTF_8 = "UTF-8";
+    /** GBK */
+    public static final String GBK = "GBK";
+
+    /** ISO-8859-1 */
+    public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1);
+    /** UTF-8 */
+    public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8);
+    /** GBK */
+    public static final Charset CHARSET_GBK = Charset.forName(GBK);
+
+    /**
+     * 转换为Charset对象
+     * 
+     * @param charset 字符集,为空则返回默认字符集
+     * @return Charset
+     */
+    public static Charset charset(String charset)
+    {
+        return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset);
+    }
+
+    /**
+     * 转换字符串的字符集编码
+     * 
+     * @param source 字符串
+     * @param srcCharset 源字符集,默认ISO-8859-1
+     * @param destCharset 目标字符集,默认UTF-8
+     * @return 转换后的字符集
+     */
+    public static String convert(String source, String srcCharset, String destCharset)
+    {
+        return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset));
+    }
+
+    /**
+     * 转换字符串的字符集编码
+     * 
+     * @param source 字符串
+     * @param srcCharset 源字符集,默认ISO-8859-1
+     * @param destCharset 目标字符集,默认UTF-8
+     * @return 转换后的字符集
+     */
+    public static String convert(String source, Charset srcCharset, Charset destCharset)
+    {
+        if (null == srcCharset)
+        {
+            srcCharset = StandardCharsets.ISO_8859_1;
+        }
+
+        if (null == destCharset)
+        {
+            destCharset = StandardCharsets.UTF_8;
+        }
+
+        if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset))
+        {
+            return source;
+        }
+        return new String(source.getBytes(srcCharset), destCharset);
+    }
+
+    /**
+     * @return 系统字符集编码
+     */
+    public static String systemCharset()
+    {
+        return Charset.defaultCharset().name();
+    }
+}

+ 1010 - 0
fs-service/src/main/java/com/fs/aiSipCall/utils/Convert.java

@@ -0,0 +1,1010 @@
+package com.fs.aiSipCall.utils;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.text.NumberFormat;
+import java.util.Set;
+
+/**
+ * 类型转换器
+ *
+ * @author ruoyi
+ */
+public class Convert
+{
+    /**
+     * 转换为字符串<br>
+     * 如果给定的值为null,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static String toStr(Object value, String defaultValue)
+    {
+        if (null == value)
+        {
+            return defaultValue;
+        }
+        if (value instanceof String)
+        {
+            return (String) value;
+        }
+        return value.toString();
+    }
+
+    /**
+     * 转换为字符串<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static String toStr(Object value)
+    {
+        return toStr(value, null);
+    }
+
+    /**
+     * 转换为字符<br>
+     * 如果给定的值为null,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Character toChar(Object value, Character defaultValue)
+    {
+        if (null == value)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Character)
+        {
+            return (Character) value;
+        }
+
+        final String valueStr = toStr(value, null);
+        return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0);
+    }
+
+    /**
+     * 转换为字符<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Character toChar(Object value)
+    {
+        return toChar(value, null);
+    }
+
+    /**
+     * 转换为byte<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Byte toByte(Object value, Byte defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Byte)
+        {
+            return (Byte) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).byteValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Byte.parseByte(valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为byte<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Byte toByte(Object value)
+    {
+        return toByte(value, null);
+    }
+
+    /**
+     * 转换为Short<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Short toShort(Object value, Short defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Short)
+        {
+            return (Short) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).shortValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Short.parseShort(valueStr.trim());
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为Short<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Short toShort(Object value)
+    {
+        return toShort(value, null);
+    }
+
+    /**
+     * 转换为Number<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Number toNumber(Object value, Number defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Number)
+        {
+            return (Number) value;
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return NumberFormat.getInstance().parse(valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为Number<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Number toNumber(Object value)
+    {
+        return toNumber(value, null);
+    }
+
+    /**
+     * 转换为int<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Integer toInt(Object value, Integer defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Integer)
+        {
+            return (Integer) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).intValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Integer.parseInt(valueStr.trim());
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为int<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Integer toInt(Object value)
+    {
+        return toInt(value, null);
+    }
+
+    /**
+     * 转换为Integer数组<br>
+     *
+     * @param str 被转换的值
+     * @return 结果
+     */
+    public static Integer[] toIntArray(String str)
+    {
+        return toIntArray(",", str);
+    }
+
+    /**
+     * 转换为Long数组<br>
+     *
+     * @param str 被转换的值
+     * @return 结果
+     */
+    public static Long[] toLongArray(String str)
+    {
+        return toLongArray(",", str);
+    }
+
+    /**
+     * 转换为Integer数组<br>
+     *
+     * @param split 分隔符
+     * @param split 被转换的值
+     * @return 结果
+     */
+    public static Integer[] toIntArray(String split, String str)
+    {
+        if (StringUtils.isEmpty(str))
+        {
+            return new Integer[] {};
+        }
+        String[] arr = str.split(split);
+        final Integer[] ints = new Integer[arr.length];
+        for (int i = 0; i < arr.length; i++)
+        {
+            final Integer v = toInt(arr[i], 0);
+            ints[i] = v;
+        }
+        return ints;
+    }
+
+    /**
+     * 转换为Long数组<br>
+     *
+     * @param split 分隔符
+     * @param str 被转换的值
+     * @return 结果
+     */
+    public static Long[] toLongArray(String split, String str)
+    {
+        if (StringUtils.isEmpty(str))
+        {
+            return new Long[] {};
+        }
+        String[] arr = str.split(split);
+        final Long[] longs = new Long[arr.length];
+        for (int i = 0; i < arr.length; i++)
+        {
+            final Long v = toLong(arr[i], null);
+            longs[i] = v;
+        }
+        return longs;
+    }
+
+    /**
+     * 转换为String数组<br>
+     *
+     * @param str 被转换的值
+     * @return 结果
+     */
+    public static String[] toStrArray(String str)
+    {
+        if (StringUtils.isEmpty(str))
+        {
+            return new String[] {};
+        }
+        return toStrArray(",", str);
+    }
+
+    /**
+     * 转换为String数组<br>
+     *
+     * @param split 分隔符
+     * @param split 被转换的值
+     * @return 结果
+     */
+    public static String[] toStrArray(String split, String str)
+    {
+        return str.split(split);
+    }
+
+    /**
+     * 转换为long<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Long toLong(Object value, Long defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Long)
+        {
+            return (Long) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).longValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            // 支持科学计数法
+            return new BigDecimal(valueStr.trim()).longValue();
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为long<br>
+     * 如果给定的值为<code>null</code>,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Long toLong(Object value)
+    {
+        return toLong(value, null);
+    }
+
+    /**
+     * 转换为double<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Double toDouble(Object value, Double defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Double)
+        {
+            return (Double) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).doubleValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            // 支持科学计数法
+            return new BigDecimal(valueStr.trim()).doubleValue();
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为double<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Double toDouble(Object value)
+    {
+        return toDouble(value, null);
+    }
+
+    /**
+     * 转换为Float<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Float toFloat(Object value, Float defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Float)
+        {
+            return (Float) value;
+        }
+        if (value instanceof Number)
+        {
+            return ((Number) value).floatValue();
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Float.parseFloat(valueStr.trim());
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为Float<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Float toFloat(Object value)
+    {
+        return toFloat(value, null);
+    }
+
+    /**
+     * 转换为boolean<br>
+     * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static Boolean toBool(Object value, Boolean defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof Boolean)
+        {
+            return (Boolean) value;
+        }
+        String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        valueStr = valueStr.trim().toLowerCase();
+        switch (valueStr)
+        {
+            case "true":
+            case "yes":
+            case "ok":
+            case "1":
+                return true;
+            case "false":
+            case "no":
+            case "0":
+                return false;
+            default:
+                return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为boolean<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static Boolean toBool(Object value)
+    {
+        return toBool(value, null);
+    }
+
+    /**
+     * 转换为Enum对象<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     *
+     * @param clazz Enum的Class
+     * @param value 值
+     * @param defaultValue 默认值
+     * @return Enum
+     */
+    public static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value, E defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (clazz.isAssignableFrom(value.getClass()))
+        {
+            @SuppressWarnings("unchecked")
+            E myE = (E) value;
+            return myE;
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return Enum.valueOf(clazz, valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为Enum对象<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     *
+     * @param clazz Enum的Class
+     * @param value 值
+     * @return Enum
+     */
+    public static <E extends Enum<E>> E toEnum(Class<E> clazz, Object value)
+    {
+        return toEnum(clazz, value, null);
+    }
+
+    /**
+     * 转换为BigInteger<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static BigInteger toBigInteger(Object value, BigInteger defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof BigInteger)
+        {
+            return (BigInteger) value;
+        }
+        if (value instanceof Long)
+        {
+            return BigInteger.valueOf((Long) value);
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return new BigInteger(valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为BigInteger<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<code>null</code><br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static BigInteger toBigInteger(Object value)
+    {
+        return toBigInteger(value, null);
+    }
+
+    /**
+     * 转换为BigDecimal<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @param defaultValue 转换错误时的默认值
+     * @return 结果
+     */
+    public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue)
+    {
+        if (value == null)
+        {
+            return defaultValue;
+        }
+        if (value instanceof BigDecimal)
+        {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Long)
+        {
+            return new BigDecimal((Long) value);
+        }
+        if (value instanceof Double)
+        {
+            return BigDecimal.valueOf((Double) value);
+        }
+        if (value instanceof Integer)
+        {
+            return new BigDecimal((Integer) value);
+        }
+        final String valueStr = toStr(value, null);
+        if (StringUtils.isEmpty(valueStr))
+        {
+            return defaultValue;
+        }
+        try
+        {
+            return new BigDecimal(valueStr);
+        }
+        catch (Exception e)
+        {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 转换为BigDecimal<br>
+     * 如果给定的值为空,或者转换失败,返回默认值<br>
+     * 转换失败不会报错
+     *
+     * @param value 被转换的值
+     * @return 结果
+     */
+    public static BigDecimal toBigDecimal(Object value)
+    {
+        return toBigDecimal(value, null);
+    }
+
+    /**
+     * 将对象转为字符串<br>
+     * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+     *
+     * @param obj 对象
+     * @return 字符串
+     */
+    public static String utf8Str(Object obj)
+    {
+        return str(obj, CharsetKit.CHARSET_UTF_8);
+    }
+
+    /**
+     * 将对象转为字符串<br>
+     * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+     *
+     * @param obj 对象
+     * @param charsetName 字符集
+     * @return 字符串
+     */
+    public static String str(Object obj, String charsetName)
+    {
+        return str(obj, Charset.forName(charsetName));
+    }
+
+    /**
+     * 将对象转为字符串<br>
+     * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法
+     *
+     * @param obj 对象
+     * @param charset 字符集
+     * @return 字符串
+     */
+    public static String str(Object obj, Charset charset)
+    {
+        if (null == obj)
+        {
+            return null;
+        }
+
+        if (obj instanceof String)
+        {
+            return (String) obj;
+        }
+        else if (obj instanceof byte[])
+        {
+            return str((byte[]) obj, charset);
+        }
+        else if (obj instanceof Byte[])
+        {
+            byte[] bytes = ArrayUtils.toPrimitive((Byte[]) obj);
+            return str(bytes, charset);
+        }
+        else if (obj instanceof ByteBuffer)
+        {
+            return str((ByteBuffer) obj, charset);
+        }
+        return obj.toString();
+    }
+
+    /**
+     * 将byte数组转为字符串
+     *
+     * @param bytes byte数组
+     * @param charset 字符集
+     * @return 字符串
+     */
+    public static String str(byte[] bytes, String charset)
+    {
+        return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset));
+    }
+
+    /**
+     * 解码字节码
+     *
+     * @param data 字符串
+     * @param charset 字符集,如果此字段为空,则解码的结果取决于平台
+     * @return 解码后的字符串
+     */
+    public static String str(byte[] data, Charset charset)
+    {
+        if (data == null)
+        {
+            return null;
+        }
+
+        if (null == charset)
+        {
+            return new String(data);
+        }
+        return new String(data, charset);
+    }
+
+    /**
+     * 将编码的byteBuffer数据转换为字符串
+     *
+     * @param data 数据
+     * @param charset 字符集,如果为空使用当前系统字符集
+     * @return 字符串
+     */
+    public static String str(ByteBuffer data, String charset)
+    {
+        if (data == null)
+        {
+            return null;
+        }
+
+        return str(data, Charset.forName(charset));
+    }
+
+    /**
+     * 将编码的byteBuffer数据转换为字符串
+     *
+     * @param data 数据
+     * @param charset 字符集,如果为空使用当前系统字符集
+     * @return 字符串
+     */
+    public static String str(ByteBuffer data, Charset charset)
+    {
+        if (null == charset)
+        {
+            charset = Charset.defaultCharset();
+        }
+        return charset.decode(data).toString();
+    }
+
+    // ----------------------------------------------------------------------- 全角半角转换
+    /**
+     * 半角转全角
+     *
+     * @param input String.
+     * @return 全角字符串.
+     */
+    public static String toSBC(String input)
+    {
+        return toSBC(input, null);
+    }
+
+    /**
+     * 半角转全角
+     *
+     * @param input String
+     * @param notConvertSet 不替换的字符集合
+     * @return 全角字符串.
+     */
+    public static String toSBC(String input, Set<Character> notConvertSet)
+    {
+        char[] c = input.toCharArray();
+        for (int i = 0; i < c.length; i++)
+        {
+            if (null != notConvertSet && notConvertSet.contains(c[i]))
+            {
+                // 跳过不替换的字符
+                continue;
+            }
+
+            if (c[i] == ' ')
+            {
+                c[i] = '\u3000';
+            }
+            else if (c[i] < '\177')
+            {
+                c[i] = (char) (c[i] + 65248);
+
+            }
+        }
+        return new String(c);
+    }
+
+    /**
+     * 全角转半角
+     *
+     * @param input String.
+     * @return 半角字符串
+     */
+    public static String toDBC(String input)
+    {
+        return toDBC(input, null);
+    }
+
+    /**
+     * 替换全角为半角
+     *
+     * @param text 文本
+     * @param notConvertSet 不替换的字符集合
+     * @return 替换后的字符
+     */
+    public static String toDBC(String text, Set<Character> notConvertSet)
+    {
+        char[] c = text.toCharArray();
+        for (int i = 0; i < c.length; i++)
+        {
+            if (null != notConvertSet && notConvertSet.contains(c[i]))
+            {
+                // 跳过不替换的字符
+                continue;
+            }
+
+            if (c[i] == '\u3000')
+            {
+                c[i] = ' ';
+            }
+            else if (c[i] > '\uFF00' && c[i] < '\uFF5F')
+            {
+                c[i] = (char) (c[i] - 65248);
+            }
+        }
+        String returnString = new String(c);
+
+        return returnString;
+    }
+
+    /**
+     * 数字金额大写转换 先写个完整的然后将如零拾替换成零
+     *
+     * @param n 数字
+     * @return 中文大写数字
+     */
+    public static String digitUppercase(double n)
+    {
+        String[] fraction = { "角", "分" };
+        String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" };
+        String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } };
+
+        String head = n < 0 ? "负" : "";
+        n = Math.abs(n);
+
+        String s = "";
+        for (int i = 0; i < fraction.length; i++)
+        {
+            // 优化double计算精度丢失问题
+            BigDecimal nNum = new BigDecimal(n);
+            BigDecimal decimal = new BigDecimal(10);
+            BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN);
+            double d = scale.doubleValue();
+            s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", "");
+        }
+        if (s.length() < 1)
+        {
+            s = "整";
+        }
+        int integerPart = (int) Math.floor(n);
+
+        for (int i = 0; i < unit[0].length && integerPart > 0; i++)
+        {
+            String p = "";
+            for (int j = 0; j < unit[1].length && n > 0; j++)
+            {
+                p = digit[integerPart % 10] + unit[1][j] + p;
+                integerPart = integerPart / 10;
+            }
+            s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s;
+        }
+        return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整");
+    }
+}

+ 239 - 0
fs-service/src/main/java/com/fs/aiSipCall/utils/DateUtils.java

@@ -0,0 +1,239 @@
+package com.fs.aiSipCall.utils;
+
+import org.apache.commons.lang3.time.DateFormatUtils;
+
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.*;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 时间工具类
+ * 
+ * @author ruoyi
+ */
+public class DateUtils extends org.apache.commons.lang3.time.DateUtils
+{
+    public static String YYYY = "yyyy";
+
+    public static String YYYY_MM = "yyyy-MM";
+
+    public static String YYYY_MM_DD = "yyyy-MM-dd";
+
+    public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
+
+    public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
+
+    private static String[] parsePatterns = {
+            "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", 
+            "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
+            "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"};
+
+    /**
+     * 获取当前Date型日期
+     * 
+     * @return Date() 当前日期
+     */
+    public static Date getNowDate()
+    {
+        return new Date();
+    }
+
+    /**
+     * 获取当前日期, 默认格式为yyyy-MM-dd
+     * 
+     * @return String
+     */
+    public static String getDate()
+    {
+        return dateTimeNow(YYYY_MM_DD);
+    }
+
+    public static final String getTime()
+    {
+        return dateTimeNow(YYYY_MM_DD_HH_MM_SS);
+    }
+
+    public static final String dateTimeNow()
+    {
+        return dateTimeNow(YYYYMMDDHHMMSS);
+    }
+
+    public static final String dateTimeNow(final String format)
+    {
+        return parseDateToStr(format, new Date());
+    }
+
+    public static final String dateTime(final Date date)
+    {
+        return parseDateToStr(YYYY_MM_DD, date);
+    }
+
+    public static final String parseDateToStr(final String format, final Date date)
+    {
+        return new SimpleDateFormat(format).format(date);
+    }
+
+    public static final Date dateTime(final String format, final String ts)
+    {
+        try
+        {
+            return new SimpleDateFormat(format).parse(ts);
+        }
+        catch (ParseException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 日期路径 即年/月/日 如2018/08/08
+     */
+    public static final String datePath()
+    {
+        Date now = new Date();
+        return DateFormatUtils.format(now, "yyyy/MM/dd");
+    }
+
+    /**
+     * 日期路径 即年/月/日 如20180808
+     */
+    public static final String dateTime()
+    {
+        Date now = new Date();
+        return DateFormatUtils.format(now, "yyyyMMdd");
+    }
+
+    /**
+     * 日期型字符串转化为日期 格式
+     */
+    public static Date parseDate(Object str)
+    {
+        if (str == null)
+        {
+            return null;
+        }
+        try
+        {
+            return parseDate(str.toString(), parsePatterns);
+        }
+        catch (ParseException e)
+        {
+            return null;
+        }
+    }
+
+    /**
+     * 获取服务器启动时间
+     */
+    public static Date getServerStartDate()
+    {
+        long time = ManagementFactory.getRuntimeMXBean().getStartTime();
+        return new Date(time);
+    }
+
+    /**
+     * 计算相差天数
+     */
+    public static int differentDaysByMillisecond(Date date1, Date date2)
+    {
+        return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
+    }
+
+    /**
+     * 计算时间差
+     *
+     * @param endDate 最后时间
+     * @param startTime 开始时间
+     * @return 时间差(天/小时/分钟)
+     */
+    public static String timeDistance(Date endDate, Date startTime)
+    {
+        long nd = 1000 * 24 * 60 * 60;
+        long nh = 1000 * 60 * 60;
+        long nm = 1000 * 60;
+        // long ns = 1000;
+        // 获得两个时间的毫秒时间差异
+        long diff = endDate.getTime() - startTime.getTime();
+        // 计算差多少天
+        long day = diff / nd;
+        // 计算差多少小时
+        long hour = diff % nd / nh;
+        // 计算差多少分钟
+        long min = diff % nd % nh / nm;
+        // 计算差多少秒//输出结果
+        // long sec = diff % nd % nh % nm / ns;
+        return day + "天" + hour + "小时" + min + "分钟";
+    }
+
+    /**
+     * 增加 LocalDateTime ==> Date
+     */
+    public static Date toDate(LocalDateTime temporalAccessor)
+    {
+        ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault());
+        return Date.from(zdt.toInstant());
+    }
+
+    /**
+     * 增加 LocalDate ==> Date
+     */
+    public static Date toDate(LocalDate temporalAccessor)
+    {
+        LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0));
+        ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
+        return Date.from(zdt.toInstant());
+    }
+
+    public static String format(Date date, String dateFormat) {
+        return DateFormatUtils.format(date, dateFormat);
+    }
+
+    /**
+     * 获取到当天时间的开始:当天 0 时 0 分 0 秒 0 毫秒
+     * @return
+     */
+    public static Long toStartTime() {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        return calendar.getTimeInMillis();
+    }
+
+    /**
+     * 格式化时间长度(秒转为可读格式,自动省略为 0 的单位)
+     * @param timeLenSeconds 时间长度(秒)
+     * @return 格式化后的时间字符串,如:1 天 2 小时 3 分 4 秒、36 秒、1 小时 5 分等
+     */
+    public static String formatTimeLength(long timeLenSeconds) {
+        if (timeLenSeconds <= 0) {
+            return "0 秒";
+        }
+
+        long days = timeLenSeconds / (24 * 3600);
+        long hours = (timeLenSeconds % (24 * 3600)) / 3600;
+        long minutes = (timeLenSeconds % 3600) / 60;
+        long seconds = timeLenSeconds % 60;
+
+        StringBuilder result = new StringBuilder();
+
+        if (days > 0) {
+            result.append(days).append("天");
+        }
+        if (hours > 0) {
+            result.append(hours).append("小时");
+        }
+        if (minutes > 0) {
+            result.append(minutes).append("分");
+        }
+        if (seconds > 0 || result.length() == 0) {
+            result.append(seconds).append("秒");
+        }
+
+        return result.toString();
+    }
+}

+ 90 - 0
fs-service/src/main/java/com/fs/aiSipCall/utils/StrFormatter.java

@@ -0,0 +1,90 @@
+package com.fs.aiSipCall.utils;
+
+/**
+ * 字符串格式化
+ * 
+ * @author ruoyi
+ */
+public class StrFormatter
+{
+    public static final String EMPTY_JSON = "{}";
+    public static final char C_BACKSLASH = '\\';
+    public static final char C_DELIM_START = '{';
+    public static final char C_DELIM_END = '}';
+
+    /**
+     * 格式化字符串<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     * 
+     * @param strPattern 字符串模板
+     * @param argArray 参数列表
+     * @return 结果
+     */
+    public static String format(final String strPattern, final Object... argArray)
+    {
+        if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray))
+        {
+            return strPattern;
+        }
+        final int strPatternLength = strPattern.length();
+
+        // 初始化定义好的长度以获得更好的性能
+        StringBuilder sbuf = new StringBuilder(strPatternLength + 50);
+
+        int handledPosition = 0;
+        int delimIndex;// 占位符所在位置
+        for (int argIndex = 0; argIndex < argArray.length; argIndex++)
+        {
+            delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition);
+            if (delimIndex == -1)
+            {
+                if (handledPosition == 0)
+                {
+                    return strPattern;
+                }
+                else
+                { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果
+                    sbuf.append(strPattern, handledPosition, strPatternLength);
+                    return sbuf.toString();
+                }
+            }
+            else
+            {
+                if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH)
+                {
+                    if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH)
+                    {
+                        // 转义符之前还有一个转义符,占位符依旧有效
+                        sbuf.append(strPattern, handledPosition, delimIndex - 1);
+                        sbuf.append(Convert.utf8Str(argArray[argIndex]));
+                        handledPosition = delimIndex + 2;
+                    }
+                    else
+                    {
+                        // 占位符被转义
+                        argIndex--;
+                        sbuf.append(strPattern, handledPosition, delimIndex - 1);
+                        sbuf.append(C_DELIM_START);
+                        handledPosition = delimIndex + 1;
+                    }
+                }
+                else
+                {
+                    // 正常占位符
+                    sbuf.append(strPattern, handledPosition, delimIndex);
+                    sbuf.append(Convert.utf8Str(argArray[argIndex]));
+                    handledPosition = delimIndex + 2;
+                }
+            }
+        }
+        // 加入最后一个占位符后所有的字符
+        sbuf.append(strPattern, handledPosition, strPattern.length());
+
+        return sbuf.toString();
+    }
+}

+ 669 - 0
fs-service/src/main/java/com/fs/aiSipCall/utils/StringUtils.java

@@ -0,0 +1,669 @@
+package com.fs.aiSipCall.utils;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+
+/**
+ * 字符串工具类
+ * 
+ * @author ruoyi
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils
+{
+    /** 空字符串 */
+    private static final String NULLSTR = "";
+
+    /** 下划线 */
+    private static final char SEPARATOR = '_';
+
+    /** 星号 */
+    private static final char ASTERISK = '*';
+
+    /**
+     * 获取参数不为空值
+     * 
+     * @param value defaultValue 要判断的value
+     * @return value 返回值
+     */
+    public static <T> T nvl(T value, T defaultValue)
+    {
+        return value != null ? value : defaultValue;
+    }
+
+    /**
+     * * 判断一个Collection是否为空, 包含List,Set,Queue
+     * 
+     * @param coll 要判断的Collection
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Collection<?> coll)
+    {
+        return isNull(coll) || coll.isEmpty();
+    }
+
+    /**
+     * * 判断一个Collection是否非空,包含List,Set,Queue
+     * 
+     * @param coll 要判断的Collection
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Collection<?> coll)
+    {
+        return !isEmpty(coll);
+    }
+
+    /**
+     * * 判断一个对象数组是否为空
+     * 
+     * @param objects 要判断的对象数组
+     ** @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Object[] objects)
+    {
+        return isNull(objects) || (objects.length == 0);
+    }
+
+    /**
+     * * 判断一个对象数组是否非空
+     * 
+     * @param objects 要判断的对象数组
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Object[] objects)
+    {
+        return !isEmpty(objects);
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     * 
+     * @param map 要判断的Map
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Map<?, ?> map)
+    {
+        return isNull(map) || map.isEmpty();
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     * 
+     * @param map 要判断的Map
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Map<?, ?> map)
+    {
+        return !isEmpty(map);
+    }
+
+    /**
+     * * 判断一个字符串是否为空串
+     * 
+     * @param str String
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(String str)
+    {
+        return isNull(str) || NULLSTR.equals(str.trim());
+    }
+
+    /**
+     * * 判断一个字符串是否为非空串
+     * 
+     * @param str String
+     * @return true:非空串 false:空串
+     */
+    public static boolean isNotEmpty(String str)
+    {
+        return !isEmpty(str);
+    }
+
+    /**
+     * * 判断一个对象是否为空
+     * 
+     * @param object Object
+     * @return true:为空 false:非空
+     */
+    public static boolean isNull(Object object)
+    {
+        return object == null;
+    }
+
+    /**
+     * * 判断一个对象是否非空
+     * 
+     * @param object Object
+     * @return true:非空 false:空
+     */
+    public static boolean isNotNull(Object object)
+    {
+        return !isNull(object);
+    }
+
+    /**
+     * * 判断一个对象是否是数组类型(Java基本型别的数组)
+     * 
+     * @param object 对象
+     * @return true:是数组 false:不是数组
+     */
+    public static boolean isArray(Object object)
+    {
+        return isNotNull(object) && object.getClass().isArray();
+    }
+
+    /**
+     * 去空格
+     */
+    public static String trim(String str)
+    {
+        return (str == null ? "" : str.trim());
+    }
+
+    /**
+     * 替换指定字符串的指定区间内字符为"*"
+     *
+     * @param str 字符串
+     * @param startInclude 开始位置(包含)
+     * @param endExclude 结束位置(不包含)
+     * @return 替换后的字符串
+     */
+    public static String hide(CharSequence str, int startInclude, int endExclude)
+    {
+        if (isEmpty(str))
+        {
+            return NULLSTR;
+        }
+        final int strLength = str.length();
+        if (startInclude > strLength)
+        {
+            return NULLSTR;
+        }
+        if (endExclude > strLength)
+        {
+            endExclude = strLength;
+        }
+        if (startInclude > endExclude)
+        {
+            // 如果起始位置大于结束位置,不替换
+            return NULLSTR;
+        }
+        final char[] chars = new char[strLength];
+        for (int i = 0; i < strLength; i++)
+        {
+            if (i >= startInclude && i < endExclude)
+            {
+                chars[i] = ASTERISK;
+            }
+            else
+            {
+                chars[i] = str.charAt(i);
+            }
+        }
+        return new String(chars);
+    }
+
+    /**
+     * 截取字符串
+     * 
+     * @param str 字符串
+     * @param start 开始
+     * @return 结果
+     */
+    public static String substring(final String str, int start)
+    {
+        if (str == null)
+        {
+            return NULLSTR;
+        }
+
+        if (start < 0)
+        {
+            start = str.length() + start;
+        }
+
+        if (start < 0)
+        {
+            start = 0;
+        }
+        if (start > str.length())
+        {
+            return NULLSTR;
+        }
+
+        return str.substring(start);
+    }
+
+    /**
+     * 截取字符串
+     * 
+     * @param str 字符串
+     * @param start 开始
+     * @param end 结束
+     * @return 结果
+     */
+    public static String substring(final String str, int start, int end)
+    {
+        if (str == null)
+        {
+            return NULLSTR;
+        }
+
+        if (end < 0)
+        {
+            end = str.length() + end;
+        }
+        if (start < 0)
+        {
+            start = str.length() + start;
+        }
+
+        if (end > str.length())
+        {
+            end = str.length();
+        }
+
+        if (start > end)
+        {
+            return NULLSTR;
+        }
+
+        if (start < 0)
+        {
+            start = 0;
+        }
+        if (end < 0)
+        {
+            end = 0;
+        }
+
+        return str.substring(start, end);
+    }
+
+    /**
+     * 格式化文本, {} 表示占位符<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     * 
+     * @param template 文本模板,被替换的部分用 {} 表示
+     * @param params 参数值
+     * @return 格式化后的文本
+     */
+    public static String format(String template, Object... params)
+    {
+        if (isEmpty(params) || isEmpty(template))
+        {
+            return template;
+        }
+        return StrFormatter.format(template, params);
+    }
+
+    /**
+     * 是否为http(s)://开头
+     * 
+     * @param link 链接
+     * @return 结果
+     */
+    public static boolean ishttp(String link)
+    {
+        return StringUtils.startsWithAny(link, "http://", "https://");
+    }
+
+    /**
+     * 字符串转set
+     * 
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return set集合
+     */
+    public static final Set<String> str2Set(String str, String sep)
+    {
+        return new HashSet<String>(str2List(str, sep, true, false));
+    }
+
+    /**
+     * 字符串转list
+     * 
+     * @param str 字符串
+     * @param sep 分隔符
+     * @param filterBlank 过滤纯空白
+     * @param trim 去掉首尾空白
+     * @return list集合
+     */
+    public static final List<String> str2List(String str, String sep, boolean filterBlank, boolean trim)
+    {
+        List<String> list = new ArrayList<String>();
+        if (StringUtils.isEmpty(str))
+        {
+            return list;
+        }
+
+        // 过滤空白字符串
+        if (filterBlank && StringUtils.isBlank(str))
+        {
+            return list;
+        }
+        String[] split = str.split(sep);
+        for (String string : split)
+        {
+            if (filterBlank && StringUtils.isBlank(string))
+            {
+                continue;
+            }
+            if (trim)
+            {
+                string = string.trim();
+            }
+            list.add(string);
+        }
+
+        return list;
+    }
+
+    /**
+     * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
+     *
+     * @param collection 给定的集合
+     * @param array 给定的数组
+     * @return boolean 结果
+     */
+    public static boolean containsAny(Collection<String> collection, String... array)
+    {
+        if (isEmpty(collection) || isEmpty(array))
+        {
+            return false;
+        }
+        else
+        {
+            for (String str : array)
+            {
+                if (collection.contains(str))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+     *
+     * @param cs 指定字符串
+     * @param searchCharSequences 需要检查的字符串数组
+     * @return 是否包含任意一个字符串
+     */
+    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences)
+    {
+        if (isEmpty(cs) || isEmpty(searchCharSequences))
+        {
+            return false;
+        }
+        for (CharSequence testStr : searchCharSequences)
+        {
+            if (containsIgnoreCase(cs, testStr))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 驼峰转下划线命名
+     */
+    public static String toUnderScoreCase(String str)
+    {
+        if (str == null)
+        {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        // 前置字符是否大写
+        boolean preCharIsUpperCase = true;
+        // 当前字符是否大写
+        boolean curreCharIsUpperCase = true;
+        // 下一字符是否大写
+        boolean nexteCharIsUpperCase = true;
+        for (int i = 0; i < str.length(); i++)
+        {
+            char c = str.charAt(i);
+            if (i > 0)
+            {
+                preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
+            }
+            else
+            {
+                preCharIsUpperCase = false;
+            }
+
+            curreCharIsUpperCase = Character.isUpperCase(c);
+
+            if (i < (str.length() - 1))
+            {
+                nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
+            }
+
+            if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase)
+            {
+                sb.append(SEPARATOR);
+            }
+            else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase)
+            {
+                sb.append(SEPARATOR);
+            }
+            sb.append(Character.toLowerCase(c));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * 是否包含字符串
+     * 
+     * @param str 验证字符串
+     * @param strs 字符串组
+     * @return 包含返回true
+     */
+    public static boolean inStringIgnoreCase(String str, String... strs)
+    {
+        if (str != null && strs != null)
+        {
+            for (String s : strs)
+            {
+                if (str.equalsIgnoreCase(trim(s)))
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 删除最后一个字符串
+     *
+     * @param str 输入字符串
+     * @param spit 以什么类型结尾的
+     * @return 截取后的字符串
+     */
+    public static String lastStringDel(String str, String spit)
+    {
+        if (!StringUtils.isEmpty(str) && str.endsWith(spit))
+        {
+            return str.subSequence(0, str.length() - 1).toString();
+        }
+        return str;
+    }
+
+    /**
+     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+     * 
+     * @param name 转换前的下划线大写方式命名的字符串
+     * @return 转换后的驼峰式命名的字符串
+     */
+    public static String convertToCamelCase(String name)
+    {
+        StringBuilder result = new StringBuilder();
+        // 快速检查
+        if (name == null || name.isEmpty())
+        {
+            // 没必要转换
+            return "";
+        }
+        else if (!name.contains("_"))
+        {
+            // 不含下划线,仅将首字母大写
+            return name.substring(0, 1).toUpperCase() + name.substring(1);
+        }
+        // 用下划线将原始字符串分割
+        String[] camels = name.split("_");
+        for (String camel : camels)
+        {
+            // 跳过原始字符串中开头、结尾的下换线或双重下划线
+            if (camel.isEmpty())
+            {
+                continue;
+            }
+            // 首字母大写
+            result.append(camel.substring(0, 1).toUpperCase());
+            result.append(camel.substring(1).toLowerCase());
+        }
+        return result.toString();
+    }
+
+    /**
+     * 驼峰式命名法
+     * 例如:user_name->userName
+     */
+    public static String toCamelCase(String s)
+    {
+        if (s == null)
+        {
+            return null;
+        }
+        if (s.indexOf(SEPARATOR) == -1)
+        {
+            return s;
+        }
+        s = s.toLowerCase();
+        StringBuilder sb = new StringBuilder(s.length());
+        boolean upperCase = false;
+        for (int i = 0; i < s.length(); i++)
+        {
+            char c = s.charAt(i);
+
+            if (c == SEPARATOR)
+            {
+                upperCase = true;
+            }
+            else if (upperCase)
+            {
+                sb.append(Character.toUpperCase(c));
+                upperCase = false;
+            }
+            else
+            {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+     * 
+     * @param str 指定字符串
+     * @param strs 需要检查的字符串数组
+     * @return 是否匹配
+     */
+    public static boolean matches(String str, List<String> strs)
+    {
+        if (isEmpty(str) || isEmpty(strs))
+        {
+            return false;
+        }
+        for (String pattern : strs)
+        {
+            if (isMatch(pattern, str))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断url是否与规则配置: 
+     * ? 表示单个字符; 
+     * * 表示一层路径内的任意字符串,不可跨层级; 
+     * ** 表示任意层路径;
+     * 
+     * @param pattern 匹配规则
+     * @param url 需要匹配的url
+     * @return
+     */
+    public static boolean isMatch(String pattern, String url)
+    {
+        AntPathMatcher matcher = new AntPathMatcher();
+        return matcher.match(pattern, url);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T cast(Object obj)
+    {
+        return (T) obj;
+    }
+
+    /**
+     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+     * 
+     * @param num 数字对象
+     * @param size 字符串指定长度
+     * @return 返回数字的字符串格式,该字符串为指定长度。
+     */
+    public static final String padl(final Number num, final int size)
+    {
+        return padl(num.toString(), size, '0');
+    }
+
+    /**
+     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+     * 
+     * @param s 原始字符串
+     * @param size 字符串指定长度
+     * @param c 用于补齐的字符
+     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+     */
+    public static final String padl(final String s, final int size, final char c)
+    {
+        final StringBuilder sb = new StringBuilder(size);
+        if (s != null)
+        {
+            final int len = s.length();
+            if (s.length() <= size)
+            {
+                for (int i = size - len; i > 0; i--)
+                {
+                    sb.append(c);
+                }
+                sb.append(s);
+            }
+            else
+            {
+                return s.substring(len - size, len);
+            }
+        }
+        else
+        {
+            for (int i = size; i > 0; i--)
+            {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+}

+ 34 - 0
fs-service/src/main/java/com/fs/aiSipCall/utils/UuidGenerator.java

@@ -0,0 +1,34 @@
+package com.fs.aiSipCall.utils;
+
+import java.util.Date;
+
+
+public class UuidGenerator {
+
+	  private static final Object syncRoot = new Object();
+	  private static final String dateFormat = "yyMMddHHmmss";
+	  private static final int maxCounter = 10000;
+      private static long lastNumber = maxCounter;
+      private static String timeStr = DateUtils.format(new Date(), dateFormat);
+      private static String callNodeNo = "0";
+
+      /// <summary>
+      /// 返回21为数字的uuid,不重复
+      /// 201707021702091002822
+      /// </summary>
+      /// <returns></returns>
+      public static String GetOneUuid()
+      {
+    	  synchronized (syncRoot)
+          {
+          	  String currentTimeStr = DateUtils.format(new Date(), dateFormat);
+              if (!timeStr.equals(currentTimeStr))
+              {
+                  lastNumber = maxCounter;
+                  timeStr = currentTimeStr;
+              }
+              lastNumber += 1;
+              return timeStr + callNodeNo + lastNumber;
+          }
+      }
+}

+ 59 - 0
fs-service/src/main/java/com/fs/aiSipCall/vo/ApiCallRecordQueryVo.java

@@ -0,0 +1,59 @@
+package com.fs.aiSipCall.vo;
+
+import com.alibaba.fastjson.JSONArray;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class ApiCallRecordQueryVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+
+    /** 通话唯一标志 */
+    private String uuid;
+
+    /** 呼入caller/AI外呼telephone/人工外呼callee */
+    private String telephone;
+
+    /** 呼入inboundTime/AI外呼calloutTime/人工外呼startTime */
+    private String calloutTime;
+
+    /** 电话应答时间 */
+    private String answeredTime;
+
+    /** 呼入hangupTime/AI外呼callEndTime/人工外呼endTime */
+    private String callEndTime;
+
+    /** 挂机原因 */
+    private String hangupCause;
+
+    /** 录音文件url访问地址 */
+    private String wavFileUrl;
+
+    /** 对话内容 */
+    private JSONArray dialogue;
+
+    /** 接听电话的分机号码 */
+    private String extnum;
+
+
+    /** 通话时长(秒) */
+    private Integer timeLen;
+
+    /** AI外呼的id */
+    private String sessionId;
+
+    /** 0. 未拨打, 1. 排队中, 2. 正在拨打  , 3. 未接通 , 6. 成功转接, 7. 线路故障 */
+    private Integer callstatus;
+
+    /** 主叫号码 */
+    private String callerNumber;
+
+    /** manual agent answered time. */
+    private String manualAnsweredTime;
+
+    /** The duration of the manual agent service time. */
+    private String manualAnsweredTimeLen;
+
+}

+ 45 - 0
fs-service/src/main/java/com/fs/aiSipCall/vo/CallPhoneExportVo.java

@@ -0,0 +1,45 @@
+package com.fs.aiSipCall.vo;
+
+//import cn.afterturn.easypoi.excel.annotation.Excel;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @Author:peicj
+ * @Description: 导出外呼号码
+ * @Date:2026/3/10 11:29
+ */
+@Data
+public class CallPhoneExportVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Excel(name = "任务名称")
+    private String batchName;
+
+    @Excel(name = "通话uuid")
+    private String uuid;
+
+    @Excel(name = "外呼号码")
+    private String telephone;
+
+    @Excel(name = "外呼状态")
+    private String callstatusName;
+
+    @Excel(name = "客户意向")
+    private String intent;
+
+    @Excel(name = "外呼时间")
+    private String calloutTimeStr;
+
+    @Excel(name = "接通时间")
+    private String answeredTimeStr;
+
+    @Excel(name = "挂机时间")
+    private String callEndTimeStr;
+
+    @Excel(name = "通话时长")
+    private String timeLenSec;
+
+}

+ 30 - 0
fs-service/src/main/java/com/fs/aiSipCall/vo/CcExtNumVo.java

@@ -0,0 +1,30 @@
+package com.fs.aiSipCall.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @Author:peicj
+ * @Description: 分机号
+ * @Date:2026/3/16 11:43
+ */
+@Data
+public class CcExtNumVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 流水编号 */
+    private Long extId;
+
+    /** 分机号 */
+    private Long extNum;
+
+    /** 分机密码 */
+    private String extPass;
+
+    /** 所属员工/绑定关系 */
+    private String userCode;
+
+    private Long pageNum;
+    private Long pageSize;
+}

+ 172 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/VoiceCloneController.java

@@ -0,0 +1,172 @@
+package com.fs.aiSoundReplication;
+
+import com.fs.aiSoundReplication.param.StatusResponse;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.service.TtsService;
+import com.fs.aiSoundReplication.service.VoiceCloneService;
+import com.fs.common.core.domain.R;
+import com.fs.fastgptApi.vo.AudioVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/voice-clone")
+@Api(tags = "声音复刻API")
+public class VoiceCloneController {
+
+    @Autowired
+    private VoiceCloneService voiceCloneService;
+    @Autowired
+    private TtsService ttsService;
+
+    @PostMapping("/synthesize")
+    @ApiOperation("文本转语音")
+    public AudioVO synthesize(
+            @ApiParam(value = "TTS请求参数", required = true)
+            @RequestBody TtsRequest request) {
+        return ttsService.textToSpeech(request);
+    }
+
+    @PostMapping("/synthesize-simple")
+    @ApiOperation("简化版文本转语音")
+    public AudioVO synthesizeSimple(
+            @ApiParam(value = "要合成的文本", required = true)
+            @RequestParam String text,
+            @ApiParam(value = "音色ID", required = true)
+            @RequestParam String voiceType,
+            @ApiParam(value = "音频格式")
+            @RequestParam(required = false, defaultValue = "mp3") String format,
+            @ApiParam(value = "语速 (0-15)")
+            @RequestParam(required = false, defaultValue = "1") Integer speed
+    ) {
+
+        TtsRequest request = new TtsRequest(
+                "", "", voiceType, text); // AppID和Token会在Service中设置
+        request.setReqId(UUID.randomUUID().toString());
+        request.setFormat(format);
+        request.setSpeed(speed);
+        return ttsService.textToSpeech(request);
+    }
+
+//    @PostMapping("/synthesize-and-download")
+//    @ApiOperation("文本转语音并下载")
+//    public R synthesizeAndDownload(
+//            @ApiParam(value = "要合成的文本", required = true)
+//            @RequestParam String text,
+//            @ApiParam(value = "音色ID", required = true)
+//            @RequestParam String voiceType,
+//            HttpServletRequest httpRequest) {
+//
+//        TtsRequest ttsRequest = new TtsRequest("", "", voiceType, text);
+//        ttsRequest.setReqId(UUID.randomUUID().toString());
+//
+//        String url = ttsService.textToSpeechStream(ttsRequest);
+//
+//        return R.ok();
+//    }
+
+//    @PostMapping("/batch-synthesize")
+//    @ApiOperation("批量文本转语音")
+//    public ResponseEntity<List<File>> batchSynthesize(
+//            @ApiParam(value = "文本列表", required = true)
+//            @RequestBody List<String> texts,
+//            @ApiParam(value = "音色ID", required = true)
+//            @RequestParam String voiceType,
+//            @ApiParam(value = "是否打包下载")
+//            @RequestParam(required = false, defaultValue = "false") Boolean zip) {
+//
+//        List<File> audioFiles = ttsService.batchTextToSpeech(texts, voiceType);
+//
+//        if (zip && !audioFiles.isEmpty()) {
+//            // 这里可以添加ZIP打包逻辑
+//            // 返回ZIP文件的ResponseEntity
+//        }
+//
+//        return ResponseEntity.ok(audioFiles);
+//    }
+
+//    @PostMapping("/synthesize-with-params")
+//    @ApiOperation("带参数的文本转语音")
+//    public TtsResponse synthesizeWithParams(
+//            @ApiParam(value = "音色ID", required = true) @RequestParam String voiceType,
+//            @ApiParam(value = "文本内容", required = true) @RequestParam String text,
+//            @ApiParam(value = "语速 (0-15)") @RequestParam(required = false) Integer speed,
+//            @ApiParam(value = "音量 (0-15)") @RequestParam(required = false) Integer volume,
+//            @ApiParam(value = "音高 (0-15)") @RequestParam(required = false) Integer pitch,
+//            @ApiParam(value = "情感参数") @RequestParam(required = false) String emotion,
+//            @ApiParam(value = "说话风格") @RequestParam(required = false) String speakingStyle) {
+//
+//        TtsRequest request = new TtsRequest("", "", voiceType, text);
+//        request.setReqId(UUID.randomUUID().toString());
+//
+//        if (speed != null) request.setSpeed(speed);
+//        if (volume != null) request.setVolume(volume);
+//        if (pitch != null) request.setPitch(pitch);
+//        if (emotion != null) request.setEmotion(emotion);
+//        if (speakingStyle != null) request.setSpeakingStyle(speakingStyle);
+//
+//        return ttsService.textToSpeech(request);
+//    }
+
+    private String getContentType(String format) {
+        switch (format.toLowerCase()) {
+            case "mp3":
+                return "audio/mpeg";
+            case "wav":
+                return "audio/wav";
+            case "pcm":
+                return "audio/L16";
+            default:
+                return "application/octet-stream";
+        }
+    }
+    @PostMapping("/upload")
+    @ApiOperation("上传音频训练音色")
+    public R uploadVoice(
+            @ApiParam(value = "音色ID", required = true) @RequestParam String speakerId,
+            @ApiParam(value = "音频文件", required = true) @RequestParam MultipartFile audioFile,
+            @ApiParam(value = "模型类型(1-ICL1.0, 4-ICL2.0)", defaultValue = "4")
+            @RequestParam(required = false) Integer modelType,
+            @ApiParam(value = "语种(0-中文, 1-英文)", defaultValue = "0")
+            @RequestParam(required = false) Integer language) {
+        return voiceCloneService.uploadVoice(speakerId, audioFile, modelType, language);
+    }
+
+    @GetMapping("/status/{speakerId}")
+    @ApiOperation("查询音色训练状态")
+    public StatusResponse getTrainingStatus(
+            @ApiParam(value = "音色ID", required = true)
+            @PathVariable String speakerId) {
+        return voiceCloneService.queryTrainingStatus(speakerId);
+    }
+
+//    @PostMapping("/upload-and-wait")
+//    @ApiOperation("上传并等待训练完成")
+//    public StatusResponse uploadAndWait(
+//            @ApiParam(value = "音色ID", required = true) @RequestParam String speakerId,
+//            @ApiParam(value = "音频文件", required = true) @RequestParam MultipartFile audioFile,
+//            @ApiParam(value = "模型类型", defaultValue = "4")
+//            @RequestParam(required = false) Integer modelType,
+//            @ApiParam(value = "语种", defaultValue = "0")
+//            @RequestParam(required = false) Integer language,
+//            @ApiParam(value = "最大等待时间(秒)", defaultValue = "600")
+//            @RequestParam(required = false) Integer maxWaitSeconds) {
+//
+//        // 1. 上传音频
+//        UploadResponse uploadResponse = voiceCloneService.uploadVoice(
+//                speakerId, audioFile, modelType, language);
+//
+//        // 2. 计算轮询参数
+//        int maxPollingTimes = maxWaitSeconds != null ? maxWaitSeconds * 1000 / 10000 : 60;
+//
+//        // 3. 轮询训练状态
+//        return voiceCloneService.pollTrainingStatus(
+//                uploadResponse.getSpeakerId(), maxPollingTimes, 10000L);
+//    }
+}

+ 23 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/OkHttpConfig.java

@@ -0,0 +1,23 @@
+package com.fs.aiSoundReplication.config;
+
+import okhttp3.ConnectionPool;
+import okhttp3.OkHttpClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+public class OkHttpConfig {
+
+    @Bean
+    public OkHttpClient okHttpClient() {
+        return new OkHttpClient.Builder()
+                .connectTimeout(30, TimeUnit.SECONDS)
+                .readTimeout(60, TimeUnit.SECONDS)
+                .writeTimeout(60, TimeUnit.SECONDS)
+                .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
+                .retryOnConnectionFailure(true)
+                .build();
+    }
+}

+ 34 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/TtsConfig.java

@@ -0,0 +1,34 @@
+package com.fs.aiSoundReplication.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "voice.clone.tts")
+public class TtsConfig {
+    // HTTP TTS接口地址
+    private String httpUrl = "https://openspeech.bytedance.com/api/v1/tts";
+
+    // 默认参数
+    private String defaultFormat = "mp3";
+    private Integer defaultSampleRate = 24000;
+    private Integer defaultSpeed = 1;
+    private Integer defaultVolume = 10;
+    private Integer defaultPitch = 10;
+    private String defaultCluster = "volcano_icl";
+
+    // 文本长度限制
+    private Integer maxTextLength = 500; // 最大文本长度
+
+    // 重试配置
+    private Integer maxRetryTimes = 3;
+    private Long retryInterval = 2000L;
+
+    // 音频保存路径
+    private String audioSavePath = "./audio/";
+
+    // 是否自动保存音频文件
+    private Boolean autoSave = true;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/config/VoiceCloneConfig.java

@@ -0,0 +1,27 @@
+package com.fs.aiSoundReplication.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "voice.clone")
+public class VoiceCloneConfig {
+    private String accessToken = "NqLzbUypz6WgbXbHO2P5DpqxSE1t-I4V";//正式环境需要换成公司豆包的信息
+    private String appId = "8505877548";//正式环境需要换成公司豆包的信息
+
+    // API地址
+    private String uploadUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload";
+    private String statusUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/status";
+
+    // 资源ID - 根据模型类型选择
+    private String resourceIdIcl1 = "seed-icl-1.0";
+    private String resourceIdIcl2 = "seed-icl-2.0";
+
+    // 重试配置
+    private Integer maxRetryTimes = 3;
+    private Long retryInterval = 5000L; // 重试间隔5秒
+    private Long pollingInterval = 10000L; // 轮询间隔10秒
+    private Integer maxPollingTimes = 60; // 最多轮询60次,约10分钟
+}

+ 42 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/exception/ErrorCodeEnum.java

@@ -0,0 +1,42 @@
+package com.fs.aiSoundReplication.exception;
+
+import lombok.Getter;
+
+@Getter
+public enum ErrorCodeEnum {
+    SUCCESS(0, "成功"),
+    BAD_REQUEST_ERROR(1001, "请求参数有误"),
+    AUDIO_UPLOAD_ERROR(1101, "音频上传失败"),
+    ASR_ERROR(1102, "ASR转写失败"),
+    SID_ERROR(1103, "SID声纹检测失败"),
+    SID_FAIL_ERROR(1104, "声纹检测未通过"),
+    GET_AUDIO_DATA_ERROR(1105, "获取音频数据失败"),
+    SPEAKER_ID_DUPLICATION_ERROR(1106, "SpeakerID重复"),
+    SPEAKER_ID_NOT_FOUND_ERROR(1107, "SpeakerID未找到"),
+    AUDIO_CONVERT_ERROR(1108, "音频转码失败"),
+    WER_ERROR(1109, "WER检测错误"),
+    AED_ERROR(1111, "AED检测错误"),
+    SNR_ERROR(1112, "SNR检测错误"),
+    DENOISE_ERROR(1113, "降噪处理失败"),
+    AUDIO_QUALITY_ERROR(1114, "音频质量低"),
+    ASR_NO_SPEAKER_ERROR(1122, "未检测到人声"),
+    UPLOAD_LIMIT_ERROR(1123, "已达上传次数限制"),
+    UNKNOWN_ERROR(-1, "未知错误");
+
+    private final Integer code;
+    private final String message;
+
+    ErrorCodeEnum(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public static ErrorCodeEnum fromCode(Integer code) {
+        for (ErrorCodeEnum errorCode : values()) {
+            if (errorCode.getCode().equals(code)) {
+                return errorCode;
+            }
+        }
+        return UNKNOWN_ERROR;
+    }
+}

+ 21 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/exception/VoiceCloneException.java

@@ -0,0 +1,21 @@
+package com.fs.aiSoundReplication.exception;
+
+import lombok.Getter;
+
+@Getter
+public class VoiceCloneException extends RuntimeException {
+    private final Integer errorCode;
+    private final String errorMessage;
+
+    public VoiceCloneException(Integer errorCode, String errorMessage) {
+        super(String.format("错误码: %d, 错误信息: %s", errorCode, errorMessage));
+        this.errorCode = errorCode;
+        this.errorMessage = errorMessage;
+    }
+
+    public VoiceCloneException(String message, Throwable cause) {
+        super(message, cause);
+        this.errorCode = -1;
+        this.errorMessage = message;
+    }
+}

+ 50 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/mapper/VoiceCloneMapper.java

@@ -0,0 +1,50 @@
+package com.fs.aiSoundReplication.mapper;
+
+
+import com.fs.aiSoundReplication.param.TtsChargeParam;
+import org.apache.ibatis.annotations.*;
+
+import java.util.List;
+
+public interface VoiceCloneMapper {
+    // 插入
+    @Insert("INSERT INTO vc_tts_charge_count (company_id,company_user_id,qw_user_id, text, text_length, voice_url, create_time, version_id, duration,fastgpt_role_id) VALUES (#{param.companyId},#{param.companyUserId},#{param.qwUserId} ,#{param.text}, #{param.textLength}, #{param.voiceUrl}, #{param.createTime}, #{param.versionId}, #{param.duration},#{param.fastgptRoleId})")
+    int insert(@Param("param") TtsChargeParam param);
+
+    // 根据ID删除
+    @Delete("DELETE FROM vc_tts_charge_count WHERE id = #{id}")
+    int deleteById(@Param("id") Long id);
+
+    // 更新
+    @Update("UPDATE vc_tts_charge_count SET company_user_id=#{companyUserId}, text=#{text}, text_length=#{textLength}, voice_url=#{voiceUrl}, create_time=#{createTime}, version_id=#{versionId}, unit_price=#{unitPrice}, total_price=#{totalPrice}, duration=#{duration} WHERE id = #{id}")
+    int updateById(@Param("param") TtsChargeParam param);
+
+    // 根据ID查询
+    @Select("SELECT * FROM vc_tts_charge_count WHERE id = #{id}")
+    TtsChargeParam selectById(@Param("id") Long id);
+
+    // 查询所有
+    @Select("SELECT * FROM vc_tts_charge_count")
+    List<TtsChargeParam> selectAll();
+
+    // 条件查询
+    @Select("<script>" +
+            "SELECT * FROM vc_tts_charge_count WHERE 1=1" +
+            "<if test='companyUserId != null'> AND company_user_id = #{companyUserId}</if>" +
+            "<if test='versionId != null'> AND version_id = #{versionId}</if>" +
+            "</script>")
+    List<TtsChargeParam> selectByCondition(@Param("companyUserId") Long companyUserId, @Param("versionId") Integer versionId);
+
+    @Select("SELECT \n" +
+            "company_id as companyId,\n" +
+            "company_user_id as companyUserId," +
+            "qw_user_id as qwUserId," +
+            "fastgpt_role_id as fastgptRoleId," +
+            "create_time as createTime," +
+            "SUM(text_length) AS total_text_length\n" +
+            "FROM vc_tts_charge_count\n" +
+            "WHERE create_time  >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)\n" +
+            "  AND create_time < CURDATE()\n" +
+            "GROUP BY company_id,company_user_id,qw_user_id,fastgpt_role_id ")
+    List<TtsChargeParam> countDailyTtsWords();
+}

+ 20 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/BaseResponse.java

@@ -0,0 +1,20 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class BaseResponse {
+    @JsonProperty("BaseResp")
+    private BaseResp baseResp;
+
+    @Data
+    public static class BaseResp {
+        @JsonProperty("StatusCode")
+        private Integer statusCode;
+
+        @JsonProperty("StatusMessage")
+        private String statusMessage;
+    }
+}

+ 33 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/StatusResponse.java

@@ -0,0 +1,33 @@
+package com.fs.aiSoundReplication.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class StatusResponse extends BaseResponse {
+    @JsonProperty("speaker_id")
+    private String speakerId;
+
+    private Integer status; // 0-NotFound, 1-Training, 2-Success, 3-Failed, 4-Active
+
+    @JsonProperty("create_time")
+    private Long createTime;
+
+    private String version;
+
+    @JsonProperty("demo_audio")
+    private String demoAudio;
+
+    public String getStatusText() {
+        switch (status) {
+            case 0: return "NotFound";
+            case 1: return "Training";
+            case 2: return "Success";
+            case 3: return "Failed";
+            case 4: return "Active";
+            default: return "Unknown";
+        }
+    }
+}

+ 13 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TrainingStatusRequest.java

@@ -0,0 +1,13 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class TrainingStatusRequest {
+    private String appid;
+
+    @JsonProperty("speaker_id")
+    private String speakerId;
+}

+ 83 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsChargeParam.java

@@ -0,0 +1,83 @@
+package com.fs.aiSoundReplication.param;
+
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 语音合成计费参数
+ */
+@Data
+@Accessors(chain = true)
+public class TtsChargeParam {
+    /**
+     * 主键
+     */
+    private Long id;
+
+    /**
+     * 公司id
+     */
+    private Long companyId;
+    /**
+     * 销售用户id
+     */
+    private Long companyUserId;
+
+    /**
+     * 企微用户id
+     */
+    private Long qwUserId;
+
+    /**
+     * fastgpt表的roleid
+     */
+    private Long fastgptRoleId;
+    /**
+     * tts的文本
+     */
+    private String text;
+
+    /**
+     * 文本字数
+     */
+    private Integer textLength;
+
+    /**
+     * tts的音频url
+     */
+    private String voiceUrl;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 豆包版本
+     */
+    private Integer versionId;
+
+    /**
+     * 单字价格
+     */
+    private BigDecimal unitPrice;
+
+    /**
+     * 总价
+     */
+    private BigDecimal totalPrice;
+
+    /**
+     * 音频时长
+     */
+    private Integer duration;
+
+    /**
+     * 总字数
+     */
+    private Long totalTextLength;
+}

+ 61 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsRequest.java

@@ -0,0 +1,61 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class TtsRequest {
+    @JsonProperty("appid")
+    private String appId;
+
+    @JsonProperty("token")
+    private String token;
+
+    @JsonProperty("cluster")
+    private String cluster = "volcano_icl"; // 声音复刻必须使用此cluster 2.0 volc_voice_clone_v2
+
+    @JsonProperty("voice_type")
+    private String voiceType; // 训练好的speaker_id
+
+    private String text; // 要合成的文本
+
+    private String format = "mp3"; // 音频格式: wav, mp3, pcm
+
+    private Integer sampleRate = 24000; // 采样率
+
+    private Integer speed = 1; // 语速 (0-15)
+
+    private Integer volume = 10; // 音量 (0-15)
+
+    private Integer pitch = 10; // 音高 (0-15)
+
+    @JsonProperty("audio_encode_type")
+    private String audioEncodeType = "raw"; // raw或wav
+
+    @JsonProperty("enable_subtitle")
+    private Boolean enableSubtitle = false; // 是否开启字幕
+
+    @JsonProperty("voice_id")
+    private String voiceId; // 音色ID (可选)
+
+    private String language = "zh"; // 语言: zh, en, ja等
+
+    @JsonProperty("reqid")
+    private String reqId; // 请求ID,需要保证唯一
+
+    @JsonProperty("emotion")
+    private String emotion; // 情感参数
+
+    @JsonProperty("speaking_style")
+    private String speakingStyle; // 说话风格
+
+    // 构造函数
+    public TtsRequest(String appId, String token, String voiceType, String text) {
+        this.appId = appId;
+        this.token = token;
+        this.voiceType = voiceType;
+        this.text = text;
+        this.reqId = java.util.UUID.randomUUID().toString();
+    }
+}

+ 54 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/TtsResponse.java

@@ -0,0 +1,54 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TtsResponse extends BaseResponse {
+    private AudioData data;
+
+    @Data
+    public static class AudioData {
+        private String audio; // Base64编码的音频数据
+
+        private Double duration; // 音频时长(秒)
+
+        @JsonProperty("subtitle_info")
+        private SubtitleInfo subtitleInfo;
+
+        @JsonProperty("subtitle_url")
+        private String subtitleUrl; // 字幕文件URL
+    }
+
+    @Data
+    public static class SubtitleInfo {
+        @JsonProperty("word_list")
+        private List<WordInfo> wordList;
+    }
+
+    @Data
+    public static class WordInfo {
+        private String word; // 词语
+
+        private Double start; // 开始时间(秒)
+
+        private Double end; // 结束时间(秒)
+
+        @JsonProperty("phone_list")
+        private List<PhoneInfo> phoneList;
+    }
+
+    @Data
+    public static class PhoneInfo {
+        private String phone; // 音素
+
+        private Double start; // 开始时间(秒)
+
+        private Double end; // 结束时间(秒)
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/UploadResponse.java

@@ -0,0 +1,12 @@
+package com.fs.aiSoundReplication.param;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class UploadResponse extends BaseResponse {
+    @JsonProperty("speaker_id")
+    private String speakerId;
+}

+ 38 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/param/VoiceCloneRequest.java

@@ -0,0 +1,38 @@
+package com.fs.aiSoundReplication.param;
+
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class VoiceCloneRequest {
+    private String appid;
+
+    @JsonProperty("speaker_id")
+    private String speakerId;
+
+    private List<AudioInfo> audios;
+
+    private Integer source = 2; // 固定值2
+
+    private Integer language = 0; // 0-中文, 1-英文
+
+    @JsonProperty("model_type")
+    private Integer modelType = 4; // 默认使用ICL 2.0
+
+    @JsonProperty("extra_params")
+    private String extraParams = "{}";
+
+    @Data
+    public static class AudioInfo {
+        @JsonProperty("audio_bytes")
+        private String audioBytes; // Base64编码的音频
+
+        @JsonProperty("audio_format")
+        private String audioFormat; // wav, mp3等
+
+        private String text; // 可选,朗读文本用于校验
+    }
+}

+ 58 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/TtsService.java

@@ -0,0 +1,58 @@
+package com.fs.aiSoundReplication.service;
+
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.company.param.VcCompanyUser;
+import com.fs.fastgptApi.vo.AudioVO;
+import com.fs.qw.domain.QwUser;
+
+public interface TtsService {
+
+    /**
+     * 文本转语音
+     * @param request TTS请求参数
+     * @return TTS响应
+     */
+    AudioVO textToSpeech(TtsRequest request);
+
+    /**
+     * 简化版文本转语音
+     * @param text 要合成的文本
+     * @param voiceType 音色ID
+     * @return TTS响应
+     */
+//    TtsResponse textToSpeech(String text, String voiceType);
+
+    /**
+     * 文本转语音并保存为文件
+     * @param request TTS请求参数
+     * @param savePath 保存路径
+     * @return 保存的音频文件
+     */
+//    File textToSpeechAndSave(TtsRequest request, String savePath);
+
+    /**
+     * 文本转语音并获取字节数组
+     * @param request TTS请求参数
+     * @return 音频字节数组
+     */
+//    byte[] textToSpeechBytes(TtsRequest request);
+
+    /**
+     * 流式返回音频数据
+     * @param request TTS请求参数
+     * @return 音频资源
+     */
+//    String textToSpeechStream(TtsRequest request);
+
+    /**
+     * 批量文本转语音
+     * @param texts 文本列表
+//     * @param voiceType 音色ID
+     * @return 音频文件列表
+     */
+//    java.util.List<File> batchTextToSpeech(java.util.List<String> texts, String voiceType);
+
+    void ttsChargeByCount(VcCompanyUser vcCompanyUser, AudioVO audioVO, QwUser user);
+
+    void countDailyTtsWords();
+}

+ 51 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/VoiceCloneService.java

@@ -0,0 +1,51 @@
+package com.fs.aiSoundReplication.service;
+
+
+import com.fs.aiSoundReplication.param.StatusResponse;
+import com.fs.aiSoundReplication.param.UploadResponse;
+import com.fs.common.core.domain.R;
+import org.springframework.web.multipart.MultipartFile;
+
+public interface VoiceCloneService {
+
+    /**
+     * 上传音频训练音色
+     * @param speakerId 音色ID
+     * @param audioFile 音频文件
+     * @param modelType 模型类型 1-ICL1.0, 2-DiT标准版, 3-DiT还原版, 4-ICL2.0
+     * @param language 语种 0-中文, 1-英文等
+     * @return 上传响应
+     */
+    R uploadVoice(String speakerId, MultipartFile audioFile,
+                  Integer modelType, Integer language);
+
+    /**
+     * 上传音频训练音色(使用文件路径)
+     */
+    UploadResponse uploadVoiceByPath(String speakerId, String filePath,
+                                     Integer modelType, Integer language);
+
+    /**
+     * 查询音色训练状态
+     * @param speakerId 音色ID
+     * @return 状态响应
+     */
+    StatusResponse queryTrainingStatus(String speakerId);
+
+    /**
+     * 轮询音色训练状态
+     * @param speakerId 音色ID
+     * @param maxPollingTimes 最大轮询次数
+     * @param pollingInterval 轮询间隔(毫秒)
+     * @return 最终状态响应
+     */
+    StatusResponse pollTrainingStatus(String speakerId, Integer maxPollingTimes,
+                                      Long pollingInterval);
+
+    /**
+     * 获取资源ID(根据模型类型)
+     * @param modelType 模型类型
+     * @return 资源ID
+     */
+    String getResourceIdByModelType(Integer modelType);
+}

+ 548 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/TtsServiceImpl.java

@@ -0,0 +1,548 @@
+package com.fs.aiSoundReplication.service.impl;
+
+
+import cn.hutool.core.bean.BeanUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.aiSoundReplication.config.TtsConfig;
+import com.fs.aiSoundReplication.config.VoiceCloneConfig;
+import com.fs.aiSoundReplication.exception.VoiceCloneException;
+import com.fs.aiSoundReplication.mapper.VoiceCloneMapper;
+import com.fs.aiSoundReplication.param.TtsChargeParam;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.param.TtsResponse;
+import com.fs.aiSoundReplication.service.TtsService;
+import com.fs.company.param.VcCompanyUser;
+import com.fs.fastGpt.domain.FastgptEventLogTotal;
+import com.fs.fastGpt.mapper.FastGptRoleMapper;
+import com.fs.fastGpt.service.impl.FastgptEventLogTotalServiceImpl;
+import com.fs.fastgptApi.vo.AudioVO;
+import com.fs.qw.domain.QwUser;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static com.fs.fastgptApi.util.AudioUtils.getDurations;
+import static com.fs.fastgptApi.util.AudioUtils.transferAudioSilk;
+
+@Service("ttsService")
+@Slf4j
+public class TtsServiceImpl implements TtsService  {
+
+    @Autowired
+    private TtsConfig ttsConfig;
+
+    @Autowired
+    private VoiceCloneConfig voiceCloneConfig;
+
+    @Autowired
+    private OkHttpClient okHttpClient;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    private final ExecutorService executorService = Executors.newFixedThreadPool(5);
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+    @Autowired
+    private VoiceCloneMapper voiceCloneMapper;
+    @Autowired
+    private FastGptRoleMapper fastGptRoleMapper;
+    @Autowired
+    private FastgptEventLogTotalServiceImpl fastgptEventLogTotalServiceImpl;
+
+    @Override
+    public AudioVO textToSpeech(TtsRequest request) {
+        try {
+            // 1. 参数校验
+            validateTtsRequest(request);
+
+            // 2. 设置默认值
+            setDefaultValues(request);
+
+            // 3. 构建请求体
+            String requestBody = buildRequestBody(request);
+
+            // 4. 构建HTTP请求
+            Request httpRequest = buildHttpRequest(requestBody);
+
+            // 5. 发送请求(带重试)
+            byte[] bytes = executeTtsRequest(httpRequest);
+
+            // 6. 检查音频数据
+            if (bytes == null || bytes.length == 0) {
+                log.error("音频数据为空,VoiceType: {}", request.getVoiceType());
+                return null;
+            }
+
+            // 7. 自动保存音频文件
+            // 创建临时文件
+            File tempFile = File.createTempFile("tts_", ".wav");
+            try (FileOutputStream fos = new FileOutputStream(tempFile)) {
+                fos.write(bytes);
+            }
+            // 上传到OSS
+            try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
+                //直接转silk然后传桶,返回url     优化-需要wav格式
+                CloudStorageService storage = OSSFactory.build();
+                String wavUrl = storage.uploadSuffix(fileInputStream, ".wav");
+//                AudioVO audioVO = AudioUtils.transferAudioSilkFromUrl(wavUrl, false);
+                Integer durations = getDurations(tempFile.getParent()+"\\"+tempFile.getName());
+                String silkUrl = transferAudioSilk(tempFile.getParent()+"\\", tempFile.getName(), false);
+                AudioVO audioVO = new AudioVO();
+                audioVO.setDuration(durations);
+                audioVO.setUrl(silkUrl);
+                audioVO.setWavUrl(wavUrl);
+                audioVO.setVoiceTxt(request.getText());
+                log.info("音频文件上传OSS成功: {}", audioVO.getUrl());
+                return audioVO;
+            } finally {
+                // 删除临时文件
+                tempFile.delete();
+            }
+
+        } catch (Exception e) {
+            log.error("TTS合成失败,reqId: {}, 错误: {}",
+                    request.getReqId(), e.getMessage());
+            throw e instanceof VoiceCloneException ?
+                    (VoiceCloneException) e :
+                    new VoiceCloneException("TTS合成失败", e);
+        }
+    }
+
+    @Override
+    public void ttsChargeByCount(VcCompanyUser vcCompanyUser, AudioVO audioVO, QwUser user) {
+        try {
+            //        BigDecimal ttsCharge = getTtsCharge(texts,unitPrice); // 算钱 暂时没有确认价格,只是统计字数
+            TtsChargeParam ttsChargeParam = new TtsChargeParam();
+            ttsChargeParam.setText(audioVO.getVoiceTxt()).setTextLength(audioVO.getVoiceTxt().length()).setCreateTime(LocalDateTime.now())
+//                .setTotalPrice(ttsCharge).setUnitPrice(unitPrice)
+                    .setCompanyUserId(Long.valueOf(vcCompanyUser.getId())).setVoiceUrl(audioVO.getUrl()).setDuration(audioVO.getDuration())
+                    .setVersionId(vcCompanyUser.getVersionId());
+            if (user.getCompanyId()!=null){
+                ttsChargeParam.setCompanyId(user.getCompanyId()).setQwUserId(user.getId());
+                if (user.getFastGptRoleId()!=null){
+                    ttsChargeParam.setFastgptRoleId(user.getFastGptRoleId());
+                }
+            }
+            voiceCloneMapper.insert(ttsChargeParam);
+        }catch (Exception e){
+            log.error("豆包声音复刻计数异常",e);
+        }
+
+
+    }
+
+    @Override
+    public void countDailyTtsWords() {
+        List<TtsChargeParam> ttsChargeParams = voiceCloneMapper.countDailyTtsWords();
+        if (ttsChargeParams.isEmpty()){
+            log.info("每日统计声音复刻昨日无数据");
+            return;
+        }
+        log.info("每日统计声音复刻:"+ttsChargeParams);
+
+        ArrayList<FastgptEventLogTotal> Totals = new ArrayList<>();
+        ttsChargeParams.forEach(o->{
+            FastgptEventLogTotal bean = BeanUtil.toBean(o, FastgptEventLogTotal.class);
+            bean.setRoleId(o.getFastgptRoleId());
+            bean.setType(14);
+            bean.setCount(o.getTotalTextLength() * 450);
+            bean.setStatTime(o.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
+            Totals.add(bean);
+        });
+        fastgptEventLogTotalServiceImpl.getBaseMapper().insertFastgptEventLogTotalBatch(Totals);
+    }
+//    @Override
+//    public String textToSpeech(String text, String voiceType) {
+//        // 创建简化版请求
+//        TtsRequest request = new TtsRequest(
+//                voiceCloneConfig.getAppId(),
+//                voiceCloneConfig.getAccessToken(),
+//                voiceType,
+//                text
+//        );
+//
+//        // 设置默认参数
+//        request.setFormat(ttsConfig.getDefaultFormat());
+//        request.setSampleRate(ttsConfig.getDefaultSampleRate());
+//        request.setCluster(ttsConfig.getDefaultCluster());
+//
+//        return textToSpeech(request);
+//    }
+
+//    @Override
+//    public File textToSpeechAndSave(TtsRequest request, String savePath) {
+//        try {
+//            // 1. 执行TTS合成
+//            TtsResponse response = textToSpeech(request);
+//
+//            if (response.getData() == null || response.getData().getAudio() == null) {
+//                throw new VoiceCloneException(-1, "音频数据为空");
+//            }
+//
+//            // 2. 解码Base64音频数据
+//            byte[] audioBytes = Base64.getDecoder().decode(response.getData().getAudio());
+//
+//            // 3. 确定保存路径
+//            String finalSavePath = savePath != null ? savePath : ttsConfig.getAudioSavePath();
+//
+//            // 创建目录
+//            Path directory = Paths.get(finalSavePath);
+//            if (!Files.exists(directory)) {
+//                Files.createDirectories(directory);
+//            }
+//
+//            // 4. 生成文件名
+//            String fileName = String.format("%s_%s.%s",
+//                    request.getVoiceType(),
+//                    request.getReqId().substring(0, 8),
+//                    request.getFormat());
+//
+//            File audioFile = new File(finalSavePath, fileName);
+//
+//            // 5. 保存文件
+//            try (FileOutputStream fos = new FileOutputStream(audioFile)) {
+//                fos.write(audioBytes);
+//            }
+//
+//            log.info("音频文件保存成功: {}, 大小: {}KB",
+//                    audioFile.getAbsolutePath(), audioBytes.length / 1024);
+//
+//            return audioFile;
+//
+//        } catch (IOException e) {
+//            log.error("保存音频文件失败", e);
+//            throw new VoiceCloneException("保存音频文件失败", e);
+//        }
+//    }
+
+//    @Override
+//    public byte[] textToSpeechBytes(TtsRequest request) {
+//         textToSpeech(request);
+//    }
+
+//    @Override
+//    public String textToSpeechStream(TtsRequest request) {
+//        byte[] audioBytes = textToSpeechBytes(request);
+//
+//    }
+
+//    @Override
+//    public List<File> batchTextToSpeech(List<String> texts, String voiceType) {
+//        List<CompletableFuture<File>> futures = new ArrayList<>();
+//        List<File> results = new ArrayList<>();
+//
+//        for (int i = 0; i < texts.size(); i++) {
+//            final String text = texts.get(i);
+//            final int index = i;
+//
+//            CompletableFuture<File> future = CompletableFuture.supplyAsync(() -> {
+//                try {
+//                    TtsRequest request = new TtsRequest(
+//                            voiceCloneConfig.getAppId(),
+//                            voiceCloneConfig.getAccessToken(),
+//                            voiceType,
+//                            text
+//                    );
+//                    request.setReqId(String.format("batch_%s_%d",
+//                            UUID.randomUUID().toString().substring(0, 8), index));
+//
+//                    return textToSpeechAndSave(request, null);
+//                } catch (Exception e) {
+//                    log.error("批量TTS处理失败,文本索引: {}, 错误: {}", index, e.getMessage());
+//                    return null;
+//                }
+//            }, executorService);
+//
+//            futures.add(future);
+//        }
+//
+//        // 等待所有任务完成
+//        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+//
+//        // 收集结果
+//        for (CompletableFuture<File> future : futures) {
+//            try {
+//                File file = future.get();
+//                if (file != null) {
+//                    results.add(file);
+//                }
+//            } catch (Exception e) {
+//                log.error("获取批量处理结果失败", e);
+//            }
+//        }
+//
+//        log.info("批量TTS处理完成,成功: {}/{}", results.size(), texts.size());
+//        return results;
+//    }
+
+    // ============ 私有方法 ============
+
+    private void validateTtsRequest(TtsRequest request) {
+        if (request == null) {
+            throw new VoiceCloneException(1001, "请求参数不能为空");
+        }
+
+        if (request.getText() == null || request.getText().trim().isEmpty()) {
+            throw new VoiceCloneException(1001, "文本内容不能为空");
+        }
+
+        if (request.getText().length() > ttsConfig.getMaxTextLength()) {
+            throw new VoiceCloneException(1001,
+                    String.format("文本长度超过限制(%d字符)", ttsConfig.getMaxTextLength()));
+        }
+
+        if (request.getVoiceType() == null || request.getVoiceType().trim().isEmpty()) {
+            throw new VoiceCloneException(1001, "音色ID不能为空");
+        }
+
+        if (request.getReqId() == null || request.getReqId().trim().isEmpty()) {
+            request.setReqId(UUID.randomUUID().toString());
+        }
+    }
+
+    private void setDefaultValues(TtsRequest request) {
+        if (request.getAppId() == null || request.getAppId().equals("")) {
+            request.setAppId(voiceCloneConfig.getAppId());
+        }
+
+        if (request.getToken() == null || request.getToken().equals("")) {
+            request.setToken(voiceCloneConfig.getAccessToken());
+        }
+
+        if (request.getCluster() == null) {
+            request.setCluster(ttsConfig.getDefaultCluster());
+        }
+
+        if (request.getFormat() == null) {
+            request.setFormat(ttsConfig.getDefaultFormat());
+        }
+
+        if (request.getSampleRate() == null) {
+            request.setSampleRate(ttsConfig.getDefaultSampleRate());
+        }
+
+        if (request.getSpeed() == null) {
+            request.setSpeed(ttsConfig.getDefaultSpeed());
+        }
+
+        if (request.getVolume() == null) {
+            request.setVolume(ttsConfig.getDefaultVolume());
+        }
+
+        if (request.getPitch() == null) {
+            request.setPitch(ttsConfig.getDefaultPitch());
+        }
+    }
+
+    private String buildRequestBody(TtsRequest request) throws IOException {
+        Map<String, Object> requestBody = new HashMap<>();
+
+        // 必填参数
+        HashMap<String, Object> app = new HashMap<String, Object>() {{
+            put("appid", request.getAppId());
+            put("token", request.getToken());
+            put("cluster", request.getCluster());
+        }};
+        HashMap<String, Object> user = new HashMap<String, Object>() {{
+            put("uid","01");
+        }};
+        HashMap<String, Object> audio = new HashMap<String, Object>() {{
+            put("voice_type", request.getVoiceType());
+        }};
+        if (request.getFormat() != null)audio.put("encoding", request.getFormat());
+        if (request.getSpeed() != null)audio.put("speed_ratio", request.getSpeed());
+
+        HashMap<String, Object> requestMap = new HashMap<String, Object>() {{
+            put("reqid", request.getReqId());
+            put("text", request.getText());
+            put("operation","query");
+            put("split_sentence", 1);//处理声音复刻语速过快
+        }};
+
+        requestBody.put("app",app);
+        requestBody.put("user", user);
+        requestBody.put("audio", audio);
+        requestBody.put("request", requestMap);
+
+        // 可选参数
+//        if (request.getFormat() != null) {
+//            requestBody.put("format", request.getFormat());
+//        }
+
+//        if (request.getSampleRate() != null) {
+//            requestBody.put("sample_rate", request.getSampleRate());
+//        }
+
+//        if (request.getSpeed() != null) {
+//            requestBody.put("speed", request.getSpeed());
+//        }
+
+//        if (request.getVolume() != null) {
+//            requestBody.put("volume", request.getVolume());
+//        }
+
+//        if (request.getPitch() != null) {
+//            requestBody.put("pitch", request.getPitch());
+//        }
+
+//        if (request.getAudioEncodeType() != null) {
+//            requestBody.put("audio_encode_type", request.getAudioEncodeType());
+//        }
+
+//        if (request.getEnableSubtitle() != null) {
+//            requestBody.put("enable_subtitle", request.getEnableSubtitle());
+//        }
+
+//        if (request.getVoiceId() != null) {
+//            requestBody.put("voice_id", request.getVoiceId());
+//        }
+
+//        if (request.getLanguage() != null) {
+//            requestBody.put("language", request.getLanguage());
+//        }
+
+//        if (request.getEmotion() != null) {
+//            requestBody.put("emotion", request.getEmotion());
+//        }
+
+//        if (request.getSpeakingStyle() != null) {
+//            requestBody.put("speaking_style", request.getSpeakingStyle());
+//        }
+
+        return objectMapper.writeValueAsString(requestBody);
+    }
+
+    private Request buildHttpRequest(String requestBody) {
+        RequestBody body = RequestBody.create(JSON,requestBody );
+
+        return new Request.Builder()
+                .url(ttsConfig.getHttpUrl())
+                .post(body)
+                .addHeader(AUTHORIZATION_HEADER, "Bearer;" + voiceCloneConfig.getAccessToken())
+                .addHeader("Content-Type", "application/json")
+                .build();
+    }
+
+    private byte[] executeTtsRequest(Request httpRequest) {
+        IOException lastException = null;
+
+        for (int i = 0; i < ttsConfig.getMaxRetryTimes(); i++) {
+            try (Response response = okHttpClient.newCall(httpRequest).execute()) {
+                if (!response.isSuccessful()) {
+                    throw new IOException("HTTP请求失败,状态码: " + response.code());
+                }
+
+                String responseBody = response.body().string();
+                log.debug("TTS API响应: {}", responseBody);
+
+                Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
+                Integer code = (Integer) responseMap.get("code");
+                if (code != null && code != 3000) {
+                    String message = (String) responseMap.get("message");
+                    throw new VoiceCloneException(code != null ? code : -1,
+                            String.format("TTS合成失败: %s (错误码: %d)", message, code != null ? code : -1));
+                }
+                // 获取data字段(base64编码的音频数据)
+                Object data = responseMap.get("data");
+                if (data == null) {
+                    throw new VoiceCloneException(-1, "TTS音频数据为空");
+                }
+
+                if (!(data instanceof String)) {
+                    throw new VoiceCloneException(-1, "TTS音频数据格式错误");
+                }
+
+                String base64Audio = (String) data;
+
+                // 解码base64音频数据
+                try {
+                    return Base64.getDecoder().decode(base64Audio);
+                } catch (IllegalArgumentException e) {
+                    log.error("Base64解码失败", e);
+                    throw new VoiceCloneException("音频数据解码失败", e);
+                }
+
+            } catch (IOException e) {
+                lastException = e;
+                log.warn("第{}次TTS请求失败: {}", i + 1, e.getMessage());
+
+                if (i < ttsConfig.getMaxRetryTimes() - 1) {
+                    try {
+                        Thread.sleep(ttsConfig.getRetryInterval());
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("重试被中断", ie);
+                    }
+                }
+            }
+        }
+
+        throw new VoiceCloneException("TTS请求失败,达到最大重试次数", lastException);
+    }
+
+    private void checkTtsResponse(TtsResponse response) {
+        if (response == null || response.getBaseResp() == null) {
+            throw new VoiceCloneException(-1, "TTS响应数据异常");
+        }
+
+        Integer statusCode = response.getBaseResp().getStatusCode();
+        if (statusCode != 0) {
+            String errorMessage = response.getBaseResp().getStatusMessage();
+            throw new VoiceCloneException(statusCode,
+                    String.format("TTS合成失败: %s (错误码: %d)", errorMessage, statusCode));
+        }
+
+        if (response.getData() == null) {
+            throw new VoiceCloneException(-1, "TTS音频数据为空");
+        }
+
+        if (response.getData().getAudio() == null) {
+            throw new VoiceCloneException(-1, "Base64音频数据为空");
+        }
+    }
+
+    private void autoSaveAudio(String base64Audio, String reqId, String format) {
+        try {
+            // 解码音频
+            byte[] audioBytes = Base64.getDecoder().decode(base64Audio);
+
+            // 创建保存目录
+            Path saveDir = Paths.get(ttsConfig.getAudioSavePath());
+            if (!Files.exists(saveDir)) {
+                Files.createDirectories(saveDir);
+            }
+
+            // 生成文件名
+            String fileName = String.format("auto_save_%s.%s",
+                    reqId.substring(0, 8), format);
+            Path filePath = saveDir.resolve(fileName);
+
+            // 保存文件
+            Files.write(filePath, audioBytes);
+
+            log.debug("音频自动保存成功: {}", filePath);
+
+        } catch (Exception e) {
+            log.warn("音频自动保存失败: {}", e.getMessage());
+            // 不抛出异常,自动保存失败不影响主要功能
+        }
+    }
+}

+ 349 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/service/impl/VoiceCloneServiceImpl.java

@@ -0,0 +1,349 @@
+package com.fs.aiSoundReplication.service.impl;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.aiSoundReplication.config.VoiceCloneConfig;
+import com.fs.aiSoundReplication.exception.ErrorCodeEnum;
+import com.fs.aiSoundReplication.exception.VoiceCloneException;
+import com.fs.aiSoundReplication.param.*;
+import com.fs.aiSoundReplication.service.VoiceCloneService;
+import com.fs.aiSoundReplication.util.FileUtil;
+import com.fs.common.core.domain.R;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.Collections;
+
+@Service
+@Slf4j
+public class VoiceCloneServiceImpl implements VoiceCloneService {
+
+    @Autowired
+    private VoiceCloneConfig config;
+
+    @Autowired
+    private OkHttpClient okHttpClient;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String RESOURCE_ID_HEADER = "Resource-Id";
+
+    @Override
+    public R uploadVoice(String speakerId, MultipartFile audioFile,
+                         Integer modelType, Integer language) {
+        try {
+            // 1. 参数校验
+            if (!validateUploadParams(speakerId, audioFile, modelType))
+                throw new VoiceCloneException(ErrorCodeEnum.BAD_REQUEST_ERROR.getCode(),ErrorCodeEnum.BAD_REQUEST_ERROR.getMessage());
+
+            // 2. 构建请求
+            VoiceCloneRequest request = buildUploadRequest(speakerId, audioFile, modelType, language);
+            String requestBody = objectMapper.writeValueAsString(request);
+
+            // 3. 构建请求头
+            String resourceId = getResourceIdByModelType(modelType);
+            Request httpRequest = buildHttpRequest(config.getUploadUrl(), resourceId, requestBody);
+
+            // 4. 发送请求(带重试机制)
+            UploadResponse response = executeRequestWithRetry(httpRequest, UploadResponse.class);
+
+            // 5. 检查响应
+            checkResponse(response);
+
+            log.info("音色上传成功,speakerId: {}", response.getSpeakerId());
+            return R.ok();
+
+        } catch (JsonProcessingException e) {
+            log.error("JSON序列化失败,声音复刻发送豆包失败", e);
+            return R.error("JSON序列化失败");
+        } catch (IOException e) {
+            log.error("文件处理失败,声音复刻发送豆包失败", e);
+            return R.error("文件处理失败");
+        }
+    }
+
+    @Override
+    public UploadResponse uploadVoiceByPath(String speakerId, String filePath,
+                                            Integer modelType, Integer language) {
+        try {
+            // 1. 参数校验
+            validateUploadParams(speakerId, null, modelType);
+
+            // 2. 读取文件并转换为Base64
+            String base64Audio = FileUtil.fileToBase64(filePath);
+            String fileExtension = FileUtil.getFileExtension(filePath);
+
+            // 3. 构建音频信息
+            VoiceCloneRequest.AudioInfo audioInfo = new VoiceCloneRequest.AudioInfo();
+            audioInfo.setAudioBytes(base64Audio);
+            audioInfo.setAudioFormat(fileExtension);
+
+            VoiceCloneRequest request = new VoiceCloneRequest();
+            request.setAppid(config.getAppId());
+            request.setSpeakerId(speakerId);
+            request.setAudios(Collections.singletonList(audioInfo));
+            request.setModelType(modelType);
+            request.setLanguage(language != null ? language : 0);
+            request.setSource(2);
+
+            // 4. 构建请求头并发送
+            String resourceId = getResourceIdByModelType(modelType);
+            String requestBody = objectMapper.writeValueAsString(request);
+            Request httpRequest = buildHttpRequest(config.getUploadUrl(), resourceId, requestBody);
+
+            UploadResponse response = executeRequestWithRetry(httpRequest, UploadResponse.class);
+            checkResponse(response);
+
+            log.info("音色上传成功(文件路径方式),speakerId: {}", response.getSpeakerId());
+            return response;
+
+        } catch (IOException e) {
+            log.error("文件处理失败", e);
+            throw new VoiceCloneException("文件处理失败", e);
+        }
+    }
+
+    @Override
+    public StatusResponse queryTrainingStatus(String speakerId) {
+        try {
+            // 1. 参数校验
+            if (speakerId == null || speakerId.trim().isEmpty()) {
+                throw new VoiceCloneException(1001, "speakerId不能为空");
+            }
+
+            // 2. 构建请求
+            TrainingStatusRequest request = new TrainingStatusRequest();
+            request.setAppid(config.getAppId());
+            request.setSpeakerId(speakerId);
+            String requestBody = objectMapper.writeValueAsString(request);
+
+            // 3. 根据历史记录确定资源ID,默认使用ICL 2.0
+            String resourceId = config.getResourceIdIcl2();
+            Request httpRequest = buildHttpRequest(config.getStatusUrl(), resourceId, requestBody);
+
+            // 4. 发送请求
+            StatusResponse response = executeRequest(httpRequest, StatusResponse.class);
+            checkResponse(response);
+
+            log.debug("训练状态查询成功,speakerId: {}, 状态: {}",
+                    response.getSpeakerId(), response.getStatusText());
+            return response;
+
+        } catch (IOException e) {
+            log.error("JSON序列化失败", e);
+            throw new VoiceCloneException("JSON序列化失败", e);
+        }
+    }
+
+    @Override
+    public StatusResponse pollTrainingStatus(String speakerId, Integer maxPollingTimes,
+                                             Long pollingInterval) {
+        if (maxPollingTimes == null) {
+            maxPollingTimes = config.getMaxPollingTimes();
+        }
+        if (pollingInterval == null) {
+            pollingInterval = config.getPollingInterval();
+        }
+
+        StatusResponse finalResponse = null;
+
+        for (int i = 0; i < maxPollingTimes; i++) {
+            try {
+                // 查询状态
+                StatusResponse response = queryTrainingStatus(speakerId);
+                finalResponse = response;
+
+                // 检查状态
+                Integer status = response.getStatus();
+                if (status == 2 || status == 4) {
+                    log.info("音色训练完成,speakerId: {}, 状态: {}",
+                            speakerId, response.getStatusText());
+                    return response;
+                } else if (status == 3) {
+                    log.error("音色训练失败,speakerId: {}", speakerId);
+                    throw new VoiceCloneException(ErrorCodeEnum.fromCode(status).getCode(),
+                            "训练失败,状态码: " + status);
+                } else if (status == 1) {
+                    log.info("训练中... (第{}次轮询)", i + 1);
+                    // 等待指定间隔
+                    Thread.sleep(pollingInterval);
+                } else {
+                    log.warn("未知状态,停止轮询,状态码: {}", status);
+                    return response;
+                }
+
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new VoiceCloneException("轮询被中断", e);
+            } catch (Exception e) {
+                log.error("第{}次轮询失败", i + 1, e);
+                // 非最后一次失败,继续尝试
+                if (i < maxPollingTimes - 1) {
+                    try {
+                        Thread.sleep(pollingInterval);
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("轮询被中断", ie);
+                    }
+                } else {
+                    throw e instanceof VoiceCloneException ?
+                            (VoiceCloneException) e :
+                            new VoiceCloneException("轮询失败", e);
+                }
+            }
+        }
+
+        log.warn("达到最大轮询次数仍未完成,speakerId: {}", speakerId);
+        return finalResponse;
+    }
+
+    @Override
+    public String getResourceIdByModelType(Integer modelType) {
+        if (modelType == null) {
+            return config.getResourceIdIcl2(); // 默认使用ICL 2.0
+        }
+
+        switch (modelType) {
+            case 1: // ICL 1.0
+                return config.getResourceIdIcl1();
+            case 2: // DiT标准版(使用ICL 1.0)
+            case 3: // DiT还原版(使用ICL 1.0)
+                return config.getResourceIdIcl1();
+            case 4: // ICL 2.0
+                return config.getResourceIdIcl2();
+            default:
+                log.warn("未知的modelType: {},使用默认ICL 2.0", modelType);
+                return config.getResourceIdIcl2();
+        }
+    }
+
+    // ============ 私有方法 ============
+
+    private boolean validateUploadParams(String speakerId, MultipartFile audioFile,
+                                      Integer modelType) {
+        if (speakerId == null || speakerId.trim().isEmpty()) {
+            log.error("参数 speakerId 不能为空");
+            return false;
+        }
+
+        if (audioFile != null && audioFile.isEmpty()) {
+            log.error("参数 audioFile 不能为空");
+            return false;
+        }
+
+        if (modelType != null && modelType < 0 || modelType > 4) {
+            log.error("参数 modelType 错误,应为1-4");
+            return false;
+        }
+        return true;
+    }
+
+    private VoiceCloneRequest buildUploadRequest(String speakerId, MultipartFile audioFile,
+                                                 Integer modelType, Integer language) throws IOException {
+        VoiceCloneRequest request = new VoiceCloneRequest();
+        request.setAppid(config.getAppId());
+        request.setSpeakerId(speakerId);
+        request.setModelType(modelType != null ? modelType : 4);
+        request.setLanguage(language != null ? language : 0);
+        request.setSource(2);
+
+        // 构建音频信息
+        VoiceCloneRequest.AudioInfo audioInfo = new VoiceCloneRequest.AudioInfo();
+        audioInfo.setAudioBytes(FileUtil.multipartFileToBase64(audioFile));
+
+        // 获取文件扩展名
+        String originalFilename = audioFile.getOriginalFilename();
+        String fileExtension = FileUtil.getFileExtension(originalFilename);
+        if (fileExtension != null){
+            if (fileExtension.equals("m4a") || fileExtension.equals("pcm"))
+                audioInfo.setAudioFormat(fileExtension);
+        }
+        request.setAudios(Collections.singletonList(audioInfo));
+
+//        // 设置额外参数-降噪
+//        Map<String, Object> extraParams = new HashMap<>();
+//        // ICL 2.0默认关闭降噪以获得更多细节
+//        if (modelType == null || modelType == 4) {
+//            extraParams.put("enable_audio_denoise", false);
+//        } else {
+//            extraParams.put("enable_audio_denoise", true);
+//        }
+//        request.setExtraParams(objectMapper.writeValueAsString(extraParams));
+
+        return request;
+    }
+
+    private Request buildHttpRequest(String url, String resourceId, String requestBody) {
+        RequestBody body = RequestBody.create(
+                MediaType.get("application/json; charset=utf-8"),requestBody
+        );
+
+        return new Request.Builder()
+                .url(url)
+                .post(body)
+                .addHeader(AUTHORIZATION_HEADER, "Bearer;" + config.getAccessToken())
+                .addHeader(RESOURCE_ID_HEADER, resourceId)
+                .build();
+    }
+
+    private <T extends BaseResponse> T executeRequest(Request httpRequest, Class<T> responseType)
+            throws IOException {
+        try (Response response = okHttpClient.newCall(httpRequest).execute()) {
+            if (!response.isSuccessful()) {
+                throw new IOException("HTTP请求失败,状态码: " + response.code());
+            }
+
+            String responseBody = response.body().string();
+            log.debug("API响应: {}", responseBody);
+
+            return objectMapper.readValue(responseBody, responseType);
+        }
+    }
+
+    private <T extends BaseResponse> T executeRequestWithRetry(Request httpRequest,
+                                                               Class<T> responseType) {
+        IOException lastException = null;
+
+        for (int i = 0; i < config.getMaxRetryTimes(); i++) {
+            try {
+                return executeRequest(httpRequest, responseType);
+            } catch (IOException e) {
+                lastException = e;
+                log.warn("第{}次请求失败,准备重试: {}", i + 1, e.getMessage());
+
+                if (i < config.getMaxRetryTimes() - 1) {
+                    try {
+                        Thread.sleep(config.getRetryInterval());
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new VoiceCloneException("重试被中断", ie);
+                    }
+                }
+            }
+        }
+
+        throw new VoiceCloneException("请求失败,达到最大重试次数", lastException);
+    }
+
+    private void checkResponse(BaseResponse response) {
+        if (response == null || response.getBaseResp() == null) {
+            throw new VoiceCloneException(-1, "响应数据异常");
+        }
+
+        Integer statusCode = response.getBaseResp().getStatusCode();
+        if (statusCode != 0) {
+            String errorMessage = response.getBaseResp().getStatusMessage();
+            ErrorCodeEnum errorCode = ErrorCodeEnum.fromCode(statusCode);
+
+            throw new VoiceCloneException(statusCode,
+                    String.format("%s (错误码: %d)", errorCode.getMessage(), statusCode));
+        }
+    }
+}

+ 45 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/util/FileToMultipartConverterUtil.java

@@ -0,0 +1,45 @@
+package com.fs.aiSoundReplication.util;
+
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+public class FileToMultipartConverterUtil {
+    /*file转成multipartFile*/
+    public static MultipartFile convert(File file) throws IOException {
+        if (file == null || !file.exists()) {
+            throw new IllegalArgumentException("文件不存在");
+        }
+
+        try (FileInputStream input = new FileInputStream(file)) {
+            return new MockMultipartFile(
+                    file.getName(),           // 文件名
+                    file.getName(),           // 原始文件名(通常与文件名相同)
+                    getContentType(file),     // 内容类型
+                    input                     // 文件输入流
+            );
+        }
+    }
+
+    private static String getContentType(File file) {
+        String fileName = file.getName();
+        if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
+            return "image/jpeg";
+        } else if (fileName.endsWith(".png")) {
+            return "image/png";
+        } else if (fileName.endsWith(".pdf")) {
+            return "application/pdf";
+        } else if (fileName.endsWith(".txt")) {
+            return "text/plain";
+        } else if (fileName.endsWith(".mp3")) {
+            return "audio/mpeg";
+        } else if (fileName.endsWith(".mp4")) {
+            return "video/mp4";
+        }
+        // 默认类型
+        return "application/octet-stream";
+    }
+}

+ 47 - 0
fs-service/src/main/java/com/fs/aiSoundReplication/util/FileUtil.java

@@ -0,0 +1,47 @@
+package com.fs.aiSoundReplication.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Base64;
+
+@Slf4j
+public class FileUtil {
+
+    public static String fileToBase64(String filePath) throws IOException {
+        File file = new File(filePath);
+        if (!file.exists() || !file.isFile()) {
+            throw new IOException("文件不存在: " + filePath);
+        }
+
+        if (file.length() > 10 * 1024 * 1024) { // 10MB限制
+            throw new IOException("文件大小超过10MB限制");
+        }
+
+        byte[] bytes = Files.readAllBytes(file.toPath());
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    public static String getFileExtension(String fileName) {
+        if (fileName == null || !fileName.contains(".")) {
+            return "";
+        }
+        return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
+    }
+
+    public static String multipartFileToBase64(MultipartFile file) throws IOException {
+        if (file.isEmpty()) {
+            throw new IOException("文件为空");
+        }
+
+        if (file.getSize() > 10 * 1024 * 1024) { // 10MB限制
+            throw new IOException("文件大小超过10MB限制");
+        }
+
+        byte[] bytes = file.getBytes();
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+}

+ 3 - 0
fs-service/src/main/java/com/fs/aicall/domain/BaseDomain.java

@@ -1,5 +1,6 @@
 package com.fs.aicall.domain;
 
+import com.alibaba.fastjson.JSONObject;
 import lombok.Data;
 
 @Data
@@ -7,4 +8,6 @@ public class BaseDomain {
     private String seq;
     private String userData;
 
+    private JSONObject telData;
+
 }

+ 162 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcCallTask.java

@@ -0,0 +1,162 @@
+package com.fs.aicall.domain;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 外呼任务对象 cc_call_task
+ * 
+ * @author ruoyi
+ * @date 2025-05-29
+ */
+@Data
+@Accessors(chain = true)
+public class CcCallTask implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**  */
+    private Long batchId;
+
+    /** 外呼任务的业务组 */
+    @Excel(name = "外呼任务的业务组")
+    private String groupId;
+
+    /**  */
+    @Excel(name = "")
+    private String batchName;
+
+    /** 是否启动任务, 1 启动, 0 停止 */
+    @Excel(name = "是否启动任务, 1 启动, 0 停止")
+    private Integer ifcall;
+
+    /** 外呼速率 */
+    @Excel(name = "外呼速率")
+    private Double rate;
+
+    /** 当前任务最大可用外线数 */
+    @Excel(name = "当前任务最大可用外线数")
+    private Long threadNum;
+
+    /** 创建时间 */
+    @Excel(name = "创建时间")
+    private Long createtime;
+
+    /** 任务是否正在执行; */
+    @Excel(name = "任务是否正在执行;")
+    private Long executing;
+
+    /** 任务停止时间 */
+    @Excel(name = "任务停止时间")
+    private Long stopTime;
+
+    /** 任务创建者用户id */
+    @Excel(name = "任务创建者用户id")
+    private String userid;
+
+    /** 0 Pure manual outbound call; 1 AI outbound calling; 2 voice call notification. */
+    @Excel(name = "0 Pure manual outbound call; 1 AI outbound calling; 2 voice call notification.")
+    private Integer taskType;
+
+    /** 使用哪条线路外呼 */
+    @Excel(name = "使用哪条线路外呼")
+    private Long gatewayId;
+
+    /** 音色 */
+    @Excel(name = "音色")
+    private String voiceCode;
+
+    /** 音源 */
+    @Excel(name = "音源")
+    private String voiceSource;
+
+    /** The average ringing duration of the call; seconds */
+    @Excel(name = "The average ringing duration of the call; seconds")
+    private Double avgRingTimeLen;
+
+    /** The average pure call duration per call; seconds */
+    @Excel(name = "The average pure call duration per call; seconds")
+    private Double avgCallTalkTimeLen;
+
+    /** The duration of form filling after the call ends; seconds */
+    @Excel(name = "The duration of form filling after the call ends; seconds")
+    private Double avgCallEndProcessTimeLen;
+
+    /** 外呼节点 */
+    @Excel(name = "外呼节点")
+    private String callNodeNo;
+
+    /** 大模型底座账号的Id */
+    @Excel(name = "大模型底座账号的Id")
+    private Integer llmAccountId;
+
+    /** 播放次数 */
+    @Excel(name = "播放次数")
+    private Integer playTimes;
+
+    /** 请求参数 */
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    private Map<String, Object> params = new HashMap<>();
+
+    /** 总名单量 */
+    private Integer phoneCount;
+
+    /** 未拨打名单量 */
+    private Integer noCallCount;
+
+    /** 已拨打名单量 */
+    private Integer callCount;
+
+    /** 接通名单量 */
+    private Integer connectCount;
+
+    /** 未接通名单量 */
+    private Integer noConnectCount;
+
+    /** 实际接通率 */
+    private Double realConnectRate;
+
+    /** 预估接通率 (百分数格式)*/
+    @Excel(name = "预估接通率")
+    private Integer conntectRate;
+
+//    /** tts provider */
+//    private String provider;
+
+    /** tts provider */
+    private String asrProvider;
+
+    /** aiTransferType */
+    private String aiTransferType;
+
+    /** aiTransferData */
+    private String aiTransferData;
+
+    /** aiTransferGroupId */
+    private String aiTransferGroupId;
+
+    /** aiTransferGatewayId */
+    private String aiTransferGatewayId;
+
+    /** aiTransferGatewayDestNumber */
+    private String aiTransferGatewayDestNumber;
+
+    /** aiTransferExtNumber */
+    private String aiTransferExtNumber;
+
+    /** 播放次数 */
+    @Excel(name = "播放次数")
+    private Integer autoStop;
+
+    /** ivrId */
+    private String ivrId;
+
+    /** 是否允许删除 */
+    private Integer allowDel;
+
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff