Parcourir la source

增加手动sip外呼

zyy il y a 3 semaines
Parent
commit
bdf1f1f34e
69 fichiers modifiés avec 8793 ajouts et 1 suppressions
  1. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallBizGroupController.java
  2. 101 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallGatewayController.java
  3. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallLlmAgentAccountController.java
  4. 167 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java
  5. 163 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallPhoneController.java
  6. 257 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallTaskController.java
  7. 152 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java
  8. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallVoiceTtsAliyunController.java
  9. 134 0
      fs-service/src/main/java/com/fs/aiSipCall/RemoteCommon.java
  10. 38 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallBizGroup.java
  11. 85 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallGateway.java
  12. 70 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallLlmAgentAccount.java
  13. 97 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallOutboundCdr.java
  14. 270 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallPhone.java
  15. 164 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallTask.java
  16. 99 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java
  17. 50 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallVoiceTtsAliyun.java
  18. 55 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/CcCustCallRecord.java
  19. 86 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/CcCustInfo.java
  20. 16 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/AiCallListModel.java
  21. 24 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CallTaskStatModel.java
  22. 14 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CommonCallListModel.java
  23. 13 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CommonPhoneModel.java
  24. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallBizGroupMapper.java
  25. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallGatewayMapper.java
  26. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallLlmAgentAccountMapper.java
  27. 71 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallOutboundCdrMapper.java
  28. 96 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallPhoneMapper.java
  29. 67 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallTaskMapper.java
  30. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallUserMapper.java
  31. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallVoiceTtsAliyunMapper.java
  32. 21 0
      fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordByUuidQueryParams.java
  33. 36 0
      fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordQueryParams.java
  34. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallBizGroupService.java
  35. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallGatewayService.java
  36. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallLlmAgentAccountService.java
  37. 74 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallOutboundCdrService.java
  38. 84 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallPhoneService.java
  39. 74 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallTaskService.java
  40. 72 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java
  41. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallVoiceTtsAliyunService.java
  42. 110 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallBizGroupServiceImpl.java
  43. 112 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallGatewayServiceImpl.java
  44. 108 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallLlmAgentAccountServiceImpl.java
  45. 543 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java
  46. 429 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallPhoneServiceImpl.java
  47. 278 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallTaskServiceImpl.java
  48. 183 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java
  49. 108 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallVoiceTtsAliyunServiceImpl.java
  50. 85 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/CharsetKit.java
  51. 1010 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/Convert.java
  52. 239 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/DateUtils.java
  53. 90 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/StrFormatter.java
  54. 669 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/StringUtils.java
  55. 34 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/UuidGenerator.java
  56. 59 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/ApiCallRecordQueryVo.java
  57. 45 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/CallPhoneExportVo.java
  58. 30 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/CcExtNumVo.java
  59. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  60. 5 0
      fs-service/src/main/java/com/fs/company/vo/CompanyUserQwListVO.java
  61. 75 0
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallBizGroupMapper.xml
  62. 130 0
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallGatewayMapper.xml
  63. 111 0
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallLlmAgentAccountMapper.xml
  64. 127 0
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallOutboundCdrMapper.xml
  65. 251 0
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallPhoneMapper.xml
  66. 203 0
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallTaskMapper.xml
  67. 167 0
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallUserMapper.xml
  68. 86 0
      fs-service/src/main/resources/mapper/aiSipCall/AiSipCallVoiceTtsAliyunMapper.xml
  69. 2 1
      fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

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

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

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

@@ -359,4 +359,9 @@ public interface CompanyUserMapper
     int updateVcCompanyUser(@Param("vcCompanyUser") VcCompanyUser vcCompanyUser);
 
     int unbindCidServer(@Param("companyUserId") Long companyUserId);
+
+    @Update("<script>" +
+            "update company_user set ai_sip_call_user_id=#{aiSipCallId} where user_id=#{companyUserId} " +
+            "</script>")
+    public int updateCompanyUserByAiSipCall(@Param("companyUserId") Long companyUserId, @Param("aiSipCallId") Long aiSipCallId);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyUserQwListVO.java

@@ -139,4 +139,9 @@ public class CompanyUserQwListVO extends BaseEntity {
 
     private Long cidServerId;
 
+    /**
+     * aiSip外呼绑定的角色
+     */
+    private Long aiSipCallUserId;
+
 }

+ 75 - 0
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallBizGroupMapper.xml

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.aiSipCall.mapper.AiSipCallBizGroupMapper">
+    
+    <resultMap type="AiSipCallBizGroup" id="AiSipCallBizGroupResult">
+        <result property="groupId"    column="group_id"    />
+        <result property="bizGroupName"    column="biz_group_name"    />
+        <result property="sortNo"    column="sort_no"    />
+        <result property="notes"    column="notes"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="remoteGroupId"    column="remote_group_id"    />
+    </resultMap>
+
+    <sql id="selectAiSipCallBizGroupVo">
+        select group_id, biz_group_name, sort_no, notes, create_time, remote_group_id from ai_sip_call_biz_group
+    </sql>
+
+    <select id="selectAiSipCallBizGroupList" parameterType="AiSipCallBizGroup" resultMap="AiSipCallBizGroupResult">
+        <include refid="selectAiSipCallBizGroupVo"/>
+        <where>  
+            <if test="bizGroupName != null  and bizGroupName != ''"> and biz_group_name like concat('%', #{bizGroupName}, '%')</if>
+            <if test="sortNo != null "> and sort_no = #{sortNo}</if>
+            <if test="notes != null  and notes != ''"> and notes = #{notes}</if>
+            <if test="remoteGroupId != null "> and remote_group_id = #{remoteGroupId}</if>
+        </where>
+    </select>
+    
+    <select id="selectAiSipCallBizGroupByGroupId" parameterType="Long" resultMap="AiSipCallBizGroupResult">
+        <include refid="selectAiSipCallBizGroupVo"/>
+        where group_id = #{groupId}
+    </select>
+        
+    <insert id="insertAiSipCallBizGroup" parameterType="AiSipCallBizGroup" useGeneratedKeys="true" keyProperty="groupId">
+        insert into ai_sip_call_biz_group
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="bizGroupName != null and bizGroupName != ''">biz_group_name,</if>
+            <if test="sortNo != null">sort_no,</if>
+            <if test="notes != null">notes,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="remoteGroupId != null">remote_group_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="bizGroupName != null and bizGroupName != ''">#{bizGroupName},</if>
+            <if test="sortNo != null">#{sortNo},</if>
+            <if test="notes != null">#{notes},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="remoteGroupId != null">#{remoteGroupId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateAiSipCallBizGroup" parameterType="AiSipCallBizGroup">
+        update ai_sip_call_biz_group
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="bizGroupName != null and bizGroupName != ''">biz_group_name = #{bizGroupName},</if>
+            <if test="sortNo != null">sort_no = #{sortNo},</if>
+            <if test="notes != null">notes = #{notes},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="remoteGroupId != null">remote_group_id = #{remoteGroupId},</if>
+        </trim>
+        where group_id = #{groupId}
+    </update>
+
+    <delete id="deleteAiSipCallBizGroupByGroupId" parameterType="Long">
+        delete from ai_sip_call_biz_group where group_id = #{groupId}
+    </delete>
+
+    <delete id="deleteAiSipCallBizGroupByGroupIds" parameterType="String">
+        delete from ai_sip_call_biz_group where group_id in 
+        <foreach item="groupId" collection="array" open="(" separator="," close=")">
+            #{groupId}
+        </foreach>
+    </delete>
+</mapper>

+ 130 - 0
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallGatewayMapper.xml

@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.aiSipCall.mapper.AiSipCallGatewayMapper">
+    
+    <resultMap type="AiSipCallGateway" id="AiSipCallGatewayResult">
+        <result property="id"    column="id"    />
+        <result property="gwName"    column="gw_name"    />
+        <result property="profileName"    column="profile_name"    />
+        <result property="caller"    column="caller"    />
+        <result property="calleePrefix"    column="callee_prefix"    />
+        <result property="gwAddr"    column="gw_addr"    />
+        <result property="codec"    column="codec"    />
+        <result property="gwDesc"    column="gw_desc"    />
+        <result property="maxConcurrency"    column="max_concurrency"    />
+        <result property="authUsername"    column="auth_username"    />
+        <result property="authPassword"    column="auth_password"    />
+        <result property="priority"    column="priority"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="register"    column="register"    />
+        <result property="configs"    column="configs"    />
+        <result property="purpose"    column="purpose"    />
+        <result property="remoteGatewayId"    column="remote_gateway_id"    />
+    </resultMap>
+
+    <sql id="selectAiSipCallGatewayVo">
+        select id, gw_name, profile_name, caller, callee_prefix, gw_addr, codec, gw_desc, max_concurrency, auth_username, auth_password, priority, update_time, register, configs, purpose, remote_gateway_id from ai_sip_call_gateway
+    </sql>
+
+    <select id="selectAiSipCallGatewayList" parameterType="AiSipCallGateway" resultMap="AiSipCallGatewayResult">
+        <include refid="selectAiSipCallGatewayVo"/>
+        <where>  
+            <if test="gwName != null  and gwName != ''"> and gw_name like concat('%', #{gwName}, '%')</if>
+            <if test="profileName != null  and profileName != ''"> and profile_name like concat('%', #{profileName}, '%')</if>
+            <if test="caller != null  and caller != ''"> and caller = #{caller}</if>
+            <if test="calleePrefix != null  and calleePrefix != ''"> and callee_prefix = #{calleePrefix}</if>
+            <if test="gwAddr != null  and gwAddr != ''"> and gw_addr = #{gwAddr}</if>
+            <if test="codec != null  and codec != ''"> and codec = #{codec}</if>
+            <if test="gwDesc != null  and gwDesc != ''"> and gw_desc = #{gwDesc}</if>
+            <if test="maxConcurrency != null "> and max_concurrency = #{maxConcurrency}</if>
+            <if test="authUsername != null  and authUsername != ''"> and auth_username like concat('%', #{authUsername}, '%')</if>
+            <if test="authPassword != null  and authPassword != ''"> and auth_password = #{authPassword}</if>
+            <if test="priority != null "> and priority = #{priority}</if>
+            <if test="register != null "> and register = #{register}</if>
+            <if test="configs != null  and configs != ''"> and configs = #{configs}</if>
+            <if test="purpose != null "> and purpose = #{purpose}</if>
+            <if test="remoteGatewayId != null "> and remote_gateway_id = #{remoteGatewayId}</if>
+        </where>
+    </select>
+    
+    <select id="selectAiSipCallGatewayById" parameterType="Long" resultMap="AiSipCallGatewayResult">
+        <include refid="selectAiSipCallGatewayVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertAiSipCallGateway" parameterType="AiSipCallGateway" useGeneratedKeys="true" keyProperty="id">
+        insert into ai_sip_call_gateway
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="gwName != null">gw_name,</if>
+            <if test="profileName != null">profile_name,</if>
+            <if test="caller != null">caller,</if>
+            <if test="calleePrefix != null">callee_prefix,</if>
+            <if test="gwAddr != null and gwAddr != ''">gw_addr,</if>
+            <if test="codec != null and codec != ''">codec,</if>
+            <if test="gwDesc != null and gwDesc != ''">gw_desc,</if>
+            <if test="maxConcurrency != null">max_concurrency,</if>
+            <if test="authUsername != null">auth_username,</if>
+            <if test="authPassword != null">auth_password,</if>
+            <if test="priority != null">priority,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="register != null">register,</if>
+            <if test="configs != null">configs,</if>
+            <if test="purpose != null">purpose,</if>
+            <if test="remoteGatewayId != null">remote_gateway_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="gwName != null">#{gwName},</if>
+            <if test="profileName != null">#{profileName},</if>
+            <if test="caller != null">#{caller},</if>
+            <if test="calleePrefix != null">#{calleePrefix},</if>
+            <if test="gwAddr != null and gwAddr != ''">#{gwAddr},</if>
+            <if test="codec != null and codec != ''">#{codec},</if>
+            <if test="gwDesc != null and gwDesc != ''">#{gwDesc},</if>
+            <if test="maxConcurrency != null">#{maxConcurrency},</if>
+            <if test="authUsername != null">#{authUsername},</if>
+            <if test="authPassword != null">#{authPassword},</if>
+            <if test="priority != null">#{priority},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="register != null">#{register},</if>
+            <if test="configs != null">#{configs},</if>
+            <if test="purpose != null">#{purpose},</if>
+            <if test="remoteGatewayId != null">#{remoteGatewayId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateAiSipCallGateway" parameterType="AiSipCallGateway">
+        update ai_sip_call_gateway
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="gwName != null">gw_name = #{gwName},</if>
+            <if test="profileName != null">profile_name = #{profileName},</if>
+            <if test="caller != null">caller = #{caller},</if>
+            <if test="calleePrefix != null">callee_prefix = #{calleePrefix},</if>
+            <if test="gwAddr != null and gwAddr != ''">gw_addr = #{gwAddr},</if>
+            <if test="codec != null and codec != ''">codec = #{codec},</if>
+            <if test="gwDesc != null and gwDesc != ''">gw_desc = #{gwDesc},</if>
+            <if test="maxConcurrency != null">max_concurrency = #{maxConcurrency},</if>
+            <if test="authUsername != null">auth_username = #{authUsername},</if>
+            <if test="authPassword != null">auth_password = #{authPassword},</if>
+            <if test="priority != null">priority = #{priority},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="register != null">register = #{register},</if>
+            <if test="configs != null">configs = #{configs},</if>
+            <if test="purpose != null">purpose = #{purpose},</if>
+            <if test="remoteGatewayId != null">remote_gateway_id = #{remoteGatewayId},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteAiSipCallGatewayById" parameterType="Long">
+        delete from ai_sip_call_gateway where id = #{id}
+    </delete>
+
+    <delete id="deleteAiSipCallGatewayByIds" parameterType="String">
+        delete from ai_sip_call_gateway where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 111 - 0
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallLlmAgentAccountMapper.xml

@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.aiSipCall.mapper.AiSipCallLlmAgentAccountMapper">
+    
+    <resultMap type="AiSipCallLlmAgentAccount" id="AiSipCallLlmAgentAccountResult">
+        <result property="id"    column="id"    />
+        <result property="accountJson"    column="account_json"    />
+        <result property="providerClassName"    column="provider_class_name"    />
+        <result property="name"    column="name"    />
+        <result property="accountEntity"    column="account_entity"    />
+        <result property="interruptFlag"    column="interrupt_flag"    />
+        <result property="interruptKeywords"    column="interrupt_keywords"    />
+        <result property="interruptIgnoreKeywords"    column="interrupt_ignore_keywords"    />
+        <result property="intentionTips"    column="intention_tips"    />
+        <result property="concurrentNum"    column="concurrent_num"    />
+        <result property="transferManualDigit"    column="transfer_manual_digit"    />
+        <result property="kbCatId"    column="kb_cat_id"    />
+        <result property="remoteLlmAgentAccountId"    column="remote_llm_agent_account_id"    />
+    </resultMap>
+
+    <sql id="selectAiSipCallLlmAgentAccountVo">
+        select id, account_json, provider_class_name, name, account_entity, interrupt_flag, interrupt_keywords, interrupt_ignore_keywords, intention_tips, concurrent_num, transfer_manual_digit, kb_cat_id, remote_llm_agent_account_id from ai_sip_call_llm_agent_account
+    </sql>
+
+    <select id="selectAiSipCallLlmAgentAccountList" parameterType="AiSipCallLlmAgentAccount" resultMap="AiSipCallLlmAgentAccountResult">
+        <include refid="selectAiSipCallLlmAgentAccountVo"/>
+        <where>  
+            <if test="accountJson != null  and accountJson != ''"> and account_json = #{accountJson}</if>
+            <if test="providerClassName != null  and providerClassName != ''"> and provider_class_name like concat('%', #{providerClassName}, '%')</if>
+            <if test="name != null  and name != ''"> and name like concat('%', #{name}, '%')</if>
+            <if test="accountEntity != null  and accountEntity != ''"> and account_entity = #{accountEntity}</if>
+            <if test="interruptFlag != null "> and interrupt_flag = #{interruptFlag}</if>
+            <if test="interruptKeywords != null  and interruptKeywords != ''"> and interrupt_keywords = #{interruptKeywords}</if>
+            <if test="interruptIgnoreKeywords != null  and interruptIgnoreKeywords != ''"> and interrupt_ignore_keywords = #{interruptIgnoreKeywords}</if>
+            <if test="intentionTips != null  and intentionTips != ''"> and intention_tips = #{intentionTips}</if>
+            <if test="concurrentNum != null "> and concurrent_num = #{concurrentNum}</if>
+            <if test="transferManualDigit != null  and transferManualDigit != ''"> and transfer_manual_digit = #{transferManualDigit}</if>
+            <if test="kbCatId != null "> and kb_cat_id = #{kbCatId}</if>
+            <if test="remoteLlmAgentAccountId != null "> and remote_llm_agent_account_id = #{remoteLlmAgentAccountId}</if>
+        </where>
+    </select>
+    
+    <select id="selectAiSipCallLlmAgentAccountById" parameterType="Long" resultMap="AiSipCallLlmAgentAccountResult">
+        <include refid="selectAiSipCallLlmAgentAccountVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertAiSipCallLlmAgentAccount" parameterType="AiSipCallLlmAgentAccount" useGeneratedKeys="true" keyProperty="id">
+        insert into ai_sip_call_llm_agent_account
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="accountJson != null">account_json,</if>
+            <if test="providerClassName != null and providerClassName != ''">provider_class_name,</if>
+            <if test="name != null and name != ''">name,</if>
+            <if test="accountEntity != null and accountEntity != ''">account_entity,</if>
+            <if test="interruptFlag != null">interrupt_flag,</if>
+            <if test="interruptKeywords != null">interrupt_keywords,</if>
+            <if test="interruptIgnoreKeywords != null">interrupt_ignore_keywords,</if>
+            <if test="intentionTips != null">intention_tips,</if>
+            <if test="concurrentNum != null">concurrent_num,</if>
+            <if test="transferManualDigit != null and transferManualDigit != ''">transfer_manual_digit,</if>
+            <if test="kbCatId != null">kb_cat_id,</if>
+            <if test="remoteLlmAgentAccountId != null">remote_llm_agent_account_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="accountJson != null">#{accountJson},</if>
+            <if test="providerClassName != null and providerClassName != ''">#{providerClassName},</if>
+            <if test="name != null and name != ''">#{name},</if>
+            <if test="accountEntity != null and accountEntity != ''">#{accountEntity},</if>
+            <if test="interruptFlag != null">#{interruptFlag},</if>
+            <if test="interruptKeywords != null">#{interruptKeywords},</if>
+            <if test="interruptIgnoreKeywords != null">#{interruptIgnoreKeywords},</if>
+            <if test="intentionTips != null">#{intentionTips},</if>
+            <if test="concurrentNum != null">#{concurrentNum},</if>
+            <if test="transferManualDigit != null and transferManualDigit != ''">#{transferManualDigit},</if>
+            <if test="kbCatId != null">#{kbCatId},</if>
+            <if test="remoteLlmAgentAccountId != null">#{remoteLlmAgentAccountId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateAiSipCallLlmAgentAccount" parameterType="AiSipCallLlmAgentAccount">
+        update ai_sip_call_llm_agent_account
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="accountJson != null">account_json = #{accountJson},</if>
+            <if test="providerClassName != null and providerClassName != ''">provider_class_name = #{providerClassName},</if>
+            <if test="name != null and name != ''">name = #{name},</if>
+            <if test="accountEntity != null and accountEntity != ''">account_entity = #{accountEntity},</if>
+            <if test="interruptFlag != null">interrupt_flag = #{interruptFlag},</if>
+            <if test="interruptKeywords != null">interrupt_keywords = #{interruptKeywords},</if>
+            <if test="interruptIgnoreKeywords != null">interrupt_ignore_keywords = #{interruptIgnoreKeywords},</if>
+            <if test="intentionTips != null">intention_tips = #{intentionTips},</if>
+            <if test="concurrentNum != null">concurrent_num = #{concurrentNum},</if>
+            <if test="transferManualDigit != null and transferManualDigit != ''">transfer_manual_digit = #{transferManualDigit},</if>
+            <if test="kbCatId != null">kb_cat_id = #{kbCatId},</if>
+            <if test="remoteLlmAgentAccountId != null">remote_llm_agent_account_id = #{remoteLlmAgentAccountId},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteAiSipCallLlmAgentAccountById" parameterType="Long">
+        delete from ai_sip_call_llm_agent_account where id = #{id}
+    </delete>
+
+    <delete id="deleteAiSipCallLlmAgentAccountByIds" parameterType="String">
+        delete from ai_sip_call_llm_agent_account where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 127 - 0
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallOutboundCdrMapper.xml

@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.aiSipCall.mapper.AiSipCallOutboundCdrMapper">
+    
+    <resultMap type="AiSipCallOutboundCdr" id="AiSipCallOutboundCdrResult">
+        <result property="id"    column="id"    />
+        <result property="caller"    column="caller"    />
+        <result property="opnum"    column="opnum"    />
+        <result property="callee"    column="callee"    />
+        <result property="startTime"    column="start_time"    />
+        <result property="answeredTime"    column="answered_time"    />
+        <result property="endTime"    column="end_time"    />
+        <result property="uuid"    column="uuid"    />
+        <result property="callType"    column="call_type"    />
+        <result property="timeLen"    column="time_len"    />
+        <result property="timeLenValid"    column="time_len_valid"    />
+        <result property="recordFilename"    column="record_filename"    />
+        <result property="chatContent"    column="chat_content"    />
+        <result property="hangupCause"    column="hangup_cause"    />
+    </resultMap>
+
+    <sql id="selectAiSipCallOutboundCdrVo">
+        select id, caller, opnum, callee, start_time, answered_time, end_time, uuid, call_type, time_len, time_len_valid, record_filename, chat_content, hangup_cause from ai_sip_call_outbound_cdr
+    </sql>
+
+    <select id="selectAiSipCallOutboundCdrList" parameterType="AiSipCallOutboundCdr" resultMap="AiSipCallOutboundCdrResult">
+        <include refid="selectAiSipCallOutboundCdrVo"/>
+        <where>  
+            <if test="caller != null  and caller != ''"> and caller = #{caller}</if>
+            <if test="opnum != null  and opnum != ''"> and opnum = #{opnum}</if>
+            <if test="callee != null  and callee != ''"> and callee = #{callee}</if>
+            <if test="startTime != null "> and start_time = #{startTime}</if>
+            <if test="answeredTime != null "> and answered_time = #{answeredTime}</if>
+            <if test="endTime != null "> and end_time = #{endTime}</if>
+            <if test="uuid != null  and uuid != ''"> and uuid = #{uuid}</if>
+            <if test="callType != null  and callType != ''"> and call_type = #{callType}</if>
+            <if test="timeLen != null "> and time_len = #{timeLen}</if>
+            <if test="timeLenValid != null "> and time_len_valid = #{timeLenValid}</if>
+            <if test="recordFilename != null  and recordFilename != ''"> and record_filename like concat('%', #{recordFilename}, '%')</if>
+            <if test="chatContent != null  and chatContent != ''"> and chat_content = #{chatContent}</if>
+            <if test="hangupCause != null  and hangupCause != ''"> and hangup_cause = #{hangupCause}</if>
+            <if test="timeLenStart != null  and timeLenStart != ''"> and time_len &gt;= #{timeLenStart}</if>
+            <if test="timeLenEnd != null  and timeLenEnd != ''"> and time_len &lt;= #{timeLenEnd}</if>
+            <if test="startTimeStartLong != null  and startTimeStartLong != ''"> and start_time &gt;= #{startTimeStartLong}</if>
+            <if test="startTimeEndLong != null  and startTimeEndLong != ''"> and start_time &lt;= #{startTimeEndLong}</if>
+            <if test="answeredTimeStartLong != null  and answeredTimeStartLong != ''"> and answered_time &gt;= #{answeredTimeStartLong}</if>
+            <if test="answeredTimeEndLong != null  and answeredTimeEndLong != ''"> and answered_time &lt;= #{answeredTimeEndLong}</if>
+            <if test="endTimeStartLong != null  and endTimeStartLong != ''"> and end_time &gt;= #{endTimeStartLong}</if>
+            <if test="endTimeEndLong != null  and endTimeEndLong != ''"> and end_time &lt;= #{endTimeEndLong}</if>
+        </where>
+         order by end_time desc
+    </select>
+    
+    <select id="selectAiSipCallOutboundCdrById" parameterType="String" resultMap="AiSipCallOutboundCdrResult">
+        <include refid="selectAiSipCallOutboundCdrVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertAiSipCallOutboundCdr" parameterType="AiSipCallOutboundCdr">
+        insert into ai_sip_call_outbound_cdr
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="id != null">id,</if>
+            <if test="caller != null and caller != ''">caller,</if>
+            <if test="opnum != null and opnum != ''">opnum,</if>
+            <if test="callee != null and callee != ''">callee,</if>
+            <if test="startTime != null">start_time,</if>
+            <if test="answeredTime != null">answered_time,</if>
+            <if test="endTime != null">end_time,</if>
+            <if test="uuid != null and uuid != ''">uuid,</if>
+            <if test="callType != null and callType != ''">call_type,</if>
+            <if test="timeLen != null">time_len,</if>
+            <if test="timeLenValid != null">time_len_valid,</if>
+            <if test="recordFilename != null and recordFilename != ''">record_filename,</if>
+            <if test="chatContent != null">chat_content,</if>
+            <if test="hangupCause != null and hangupCause != ''">hangup_cause,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="id != null">#{id},</if>
+            <if test="caller != null and caller != ''">#{caller},</if>
+            <if test="opnum != null and opnum != ''">#{opnum},</if>
+            <if test="callee != null and callee != ''">#{callee},</if>
+            <if test="startTime != null">#{startTime},</if>
+            <if test="answeredTime != null">#{answeredTime},</if>
+            <if test="endTime != null">#{endTime},</if>
+            <if test="uuid != null and uuid != ''">#{uuid},</if>
+            <if test="callType != null and callType != ''">#{callType},</if>
+            <if test="timeLen != null">#{timeLen},</if>
+            <if test="timeLenValid != null">#{timeLenValid},</if>
+            <if test="recordFilename != null and recordFilename != ''">#{recordFilename},</if>
+            <if test="chatContent != null">#{chatContent},</if>
+            <if test="hangupCause != null and hangupCause != ''">#{hangupCause},</if>
+         </trim>
+    </insert>
+
+    <update id="updateAiSipCallOutboundCdr" parameterType="AiSipCallOutboundCdr">
+        update ai_sip_call_outbound_cdr
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="caller != null and caller != ''">caller = #{caller},</if>
+            <if test="opnum != null and opnum != ''">opnum = #{opnum},</if>
+            <if test="callee != null and callee != ''">callee = #{callee},</if>
+            <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="answeredTime != null">answered_time = #{answeredTime},</if>
+            <if test="endTime != null">end_time = #{endTime},</if>
+            <if test="uuid != null and uuid != ''">uuid = #{uuid},</if>
+            <if test="callType != null and callType != ''">call_type = #{callType},</if>
+            <if test="timeLen != null">time_len = #{timeLen},</if>
+            <if test="timeLenValid != null">time_len_valid = #{timeLenValid},</if>
+            <if test="recordFilename != null and recordFilename != ''">record_filename = #{recordFilename},</if>
+            <if test="chatContent != null">chat_content = #{chatContent},</if>
+            <if test="hangupCause != null and hangupCause != ''">hangup_cause = #{hangupCause},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteAiSipCallOutboundCdrById" parameterType="String">
+        delete from ai_sip_call_outbound_cdr where id = #{id}
+    </delete>
+
+    <delete id="deleteAiSipCallOutboundCdrByIds" parameterType="String">
+        delete from ai_sip_call_outbound_cdr where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 251 - 0
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallPhoneMapper.xml

@@ -0,0 +1,251 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.aiSipCall.mapper.AiSipCallPhoneMapper">
+    
+    <resultMap type="AiSipCallPhone" id="AiSipCallPhoneResult">
+        <result property="id"    column="id"    />
+        <result property="batchId"    column="batch_id"    />
+        <result property="telephone"    column="telephone"    />
+        <result property="custName"    column="cust_name"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="callstatus"    column="callstatus"    />
+        <result property="calloutTime"    column="callout_time"    />
+        <result property="callcount"    column="callcount"    />
+        <result property="callEndTime"    column="call_end_time"    />
+        <result property="timeLen"    column="time_len"    />
+        <result property="validTimeLen"    column="valid_time_len"    />
+        <result property="uuid"    column="uuid"    />
+        <result property="connectedTime"    column="connected_time"    />
+        <result property="hangupCause"    column="hangup_cause"    />
+        <result property="answeredTime"    column="answered_time"    />
+        <result property="dialogue"    column="dialogue"    />
+        <result property="wavfile"    column="wavfile"    />
+        <result property="recordServerUrl"    column="record_server_url"    />
+        <result property="bizJson"    column="biz_json"    />
+        <result property="dialogueCount"    column="dialogue_count"    />
+        <result property="acdOpnum"    column="acd_opnum"    />
+        <result property="acdQueueTime"    column="acd_queue_time"    />
+        <result property="acdWaitTime"    column="acd_wait_time"    />
+        <result property="ttsText"    column="tts_text"    />
+        <result property="emptyNumberDetectionText"    column="empty_number_detection_text"    />
+        <result property="intent"    column="intent"    />
+        <result property="asrSeconds"    column="asr_seconds"    />
+        <result property="ttsTimes"    column="tts_times"    />
+        <result property="ttsFlowTokens"    column="tts_flow_tokens"    />
+        <result property="inputTokens"    column="input_tokens"    />
+        <result property="outputTokens"    column="output_tokens"    />
+        <result property="totalCost"    column="total_cost"    />
+        <result property="billingStatus"    column="billing_status"    />
+        <result property="callerNumber"    column="caller_number"    />
+        <result property="ivrDtmfDigits"    column="ivr_dtmf_digits"    />
+        <result property="manualAnsweredTime"    column="manual_answered_time"    />
+        <result property="manualAnsweredTimeLen"    column="manual_answered_time_len"    />
+    </resultMap>
+
+    <sql id="selectAiSipCallPhoneVo">
+        select id, batch_id, telephone, cust_name, create_time, callstatus, callout_time, callcount, call_end_time, time_len, valid_time_len, uuid, connected_time, hangup_cause, answered_time, dialogue, wavfile, record_server_url, biz_json, dialogue_count, acd_opnum, acd_queue_time, acd_wait_time, tts_text, empty_number_detection_text, intent, asr_seconds, tts_times, tts_flow_tokens, input_tokens, output_tokens, total_cost, billing_status, caller_number, ivr_dtmf_digits, manual_answered_time, manual_answered_time_len from ai_sip_call_phone
+    </sql>
+
+    <select id="selectAiSipCallPhoneList" parameterType="AiSipCallPhone" resultMap="AiSipCallPhoneResult">
+        <include refid="selectAiSipCallPhoneVo"/>
+        <where>  
+            <if test="batchId != null "> and batch_id = #{batchId}</if>
+            <if test="telephone != null  and telephone != ''"> and telephone = #{telephone}</if>
+            <if test="custName != null  and custName != ''"> and cust_name like concat('%', #{custName}, '%')</if>
+            <if test="createTime != null "> and create_time = #{createTime}</if>
+            <if test="callstatus != null "> and callstatus = #{callstatus}</if>
+            <if test="calloutTime != null "> and callout_time = #{calloutTime}</if>
+            <if test="callcount != null "> and callcount = #{callcount}</if>
+            <if test="callEndTime != null "> and call_end_time = #{callEndTime}</if>
+            <if test="timeLen != null "> and time_len = #{timeLen}</if>
+            <if test="validTimeLen != null "> and valid_time_len = #{validTimeLen}</if>
+            <if test="uuid != null  and uuid != ''"> and uuid = #{uuid}</if>
+            <if test="connectedTime != null "> and connected_time = #{connectedTime}</if>
+            <if test="hangupCause != null  and hangupCause != ''"> and hangup_cause = #{hangupCause}</if>
+            <if test="answeredTime != null "> and answered_time = #{answeredTime}</if>
+            <if test="dialogue != null  and dialogue != ''"> and dialogue = #{dialogue}</if>
+            <if test="wavfile != null  and wavfile != ''"> and wavfile = #{wavfile}</if>
+            <if test="recordServerUrl != null  and recordServerUrl != ''"> and record_server_url = #{recordServerUrl}</if>
+            <if test="bizJson != null  and bizJson != ''"> and biz_json = #{bizJson}</if>
+            <if test="dialogueCount != null "> and dialogue_count = #{dialogueCount}</if>
+            <if test="acdOpnum != null  and acdOpnum != ''"> and acd_opnum = #{acdOpnum}</if>
+            <if test="acdQueueTime != null "> and acd_queue_time = #{acdQueueTime}</if>
+            <if test="acdWaitTime != null "> and acd_wait_time = #{acdWaitTime}</if>
+            <if test="ttsText != null  and ttsText != ''"> and tts_text = #{ttsText}</if>
+            <if test="emptyNumberDetectionText != null  and emptyNumberDetectionText != ''"> and empty_number_detection_text = #{emptyNumberDetectionText}</if>
+            <if test="intent != null  and intent != ''"> and intent = #{intent}</if>
+            <if test="asrSeconds != null "> and asr_seconds = #{asrSeconds}</if>
+            <if test="ttsTimes != null "> and tts_times = #{ttsTimes}</if>
+            <if test="ttsFlowTokens != null "> and tts_flow_tokens = #{ttsFlowTokens}</if>
+            <if test="inputTokens != null "> and input_tokens = #{inputTokens}</if>
+            <if test="outputTokens != null "> and output_tokens = #{outputTokens}</if>
+            <if test="totalCost != null "> and total_cost = #{totalCost}</if>
+            <if test="billingStatus != null "> and billing_status = #{billingStatus}</if>
+            <if test="callerNumber != null  and callerNumber != ''"> and caller_number = #{callerNumber}</if>
+            <if test="ivrDtmfDigits != null  and ivrDtmfDigits != ''"> and ivr_dtmf_digits = #{ivrDtmfDigits}</if>
+            <if test="manualAnsweredTime != null "> and manual_answered_time = #{manualAnsweredTime}</if>
+            <if test="manualAnsweredTimeLen != null "> and manual_answered_time_len = #{manualAnsweredTimeLen}</if>
+            <if test="timeLenStart != null  and timeLenStart != ''"> and time_len &gt;= #{timeLenStart}</if>
+            <if test="timeLenEnd != null  and timeLenEnd != ''"> and time_len &lt;= #{timeLenEnd}</if>
+            <if test="calloutTimeStartLong != null  and calloutTimeStartLong != ''"> and callout_time &gt;= #{calloutTimeStartLong}</if>
+            <if test="calloutTimeEndLong != null  and calloutTimeEndLong != ''"> and callout_time &lt;= #{calloutTimeEndLong}</if>
+            <if test="answeredTimeStartLong != null  and answeredTimeStartLong != ''"> and answered_time &gt;= #{answeredTimeStartLong}</if>
+            <if test="answeredTimeEndLong != null  and answeredTimeEndLong != ''"> and answered_time &lt;= #{answeredTimeEndLong}</if>
+            <if test="callEndTimeStartLong != null  and callEndTimeStartLong != ''"> and call_end_time &gt;= #{callEndTimeStartLong}</if>
+            <if test="callEndTimeEndLong != null  and callEndTimeEndLong != ''"> and call_end_time &lt;= #{callEndTimeEndLong}</if>
+        </where>
+        order by call_end_time desc
+    </select>
+    
+    <select id="selectAiSipCallPhoneById" parameterType="String" resultMap="AiSipCallPhoneResult">
+        <include refid="selectAiSipCallPhoneVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertAiSipCallPhone" parameterType="AiSipCallPhone">
+        insert into ai_sip_call_phone
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="id != null">id,</if>
+            <if test="batchId != null">batch_id,</if>
+            <if test="telephone != null and telephone != ''">telephone,</if>
+            <if test="custName != null and custName != ''">cust_name,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="callstatus != null">callstatus,</if>
+            <if test="calloutTime != null">callout_time,</if>
+            <if test="callcount != null">callcount,</if>
+            <if test="callEndTime != null">call_end_time,</if>
+            <if test="timeLen != null">time_len,</if>
+            <if test="validTimeLen != null">valid_time_len,</if>
+            <if test="uuid != null and uuid != ''">uuid,</if>
+            <if test="connectedTime != null">connected_time,</if>
+            <if test="hangupCause != null and hangupCause != ''">hangup_cause,</if>
+            <if test="answeredTime != null">answered_time,</if>
+            <if test="dialogue != null">dialogue,</if>
+            <if test="wavfile != null and wavfile != ''">wavfile,</if>
+            <if test="recordServerUrl != null and recordServerUrl != ''">record_server_url,</if>
+            <if test="bizJson != null and bizJson != ''">biz_json,</if>
+            <if test="dialogueCount != null">dialogue_count,</if>
+            <if test="acdOpnum != null and acdOpnum != ''">acd_opnum,</if>
+            <if test="acdQueueTime != null">acd_queue_time,</if>
+            <if test="acdWaitTime != null">acd_wait_time,</if>
+            <if test="ttsText != null">tts_text,</if>
+            <if test="emptyNumberDetectionText != null and emptyNumberDetectionText != ''">empty_number_detection_text,</if>
+            <if test="intent != null">intent,</if>
+            <if test="asrSeconds != null">asr_seconds,</if>
+            <if test="ttsTimes != null">tts_times,</if>
+            <if test="ttsFlowTokens != null">tts_flow_tokens,</if>
+            <if test="inputTokens != null">input_tokens,</if>
+            <if test="outputTokens != null">output_tokens,</if>
+            <if test="totalCost != null">total_cost,</if>
+            <if test="billingStatus != null">billing_status,</if>
+            <if test="callerNumber != null">caller_number,</if>
+            <if test="ivrDtmfDigits != null and ivrDtmfDigits != ''">ivr_dtmf_digits,</if>
+            <if test="manualAnsweredTime != null">manual_answered_time,</if>
+            <if test="manualAnsweredTimeLen != null">manual_answered_time_len,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="id != null">#{id},</if>
+            <if test="batchId != null">#{batchId},</if>
+            <if test="telephone != null and telephone != ''">#{telephone},</if>
+            <if test="custName != null and custName != ''">#{custName},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="callstatus != null">#{callstatus},</if>
+            <if test="calloutTime != null">#{calloutTime},</if>
+            <if test="callcount != null">#{callcount},</if>
+            <if test="callEndTime != null">#{callEndTime},</if>
+            <if test="timeLen != null">#{timeLen},</if>
+            <if test="validTimeLen != null">#{validTimeLen},</if>
+            <if test="uuid != null and uuid != ''">#{uuid},</if>
+            <if test="connectedTime != null">#{connectedTime},</if>
+            <if test="hangupCause != null and hangupCause != ''">#{hangupCause},</if>
+            <if test="answeredTime != null">#{answeredTime},</if>
+            <if test="dialogue != null">#{dialogue},</if>
+            <if test="wavfile != null and wavfile != ''">#{wavfile},</if>
+            <if test="recordServerUrl != null and recordServerUrl != ''">#{recordServerUrl},</if>
+            <if test="bizJson != null and bizJson != ''">#{bizJson},</if>
+            <if test="dialogueCount != null">#{dialogueCount},</if>
+            <if test="acdOpnum != null and acdOpnum != ''">#{acdOpnum},</if>
+            <if test="acdQueueTime != null">#{acdQueueTime},</if>
+            <if test="acdWaitTime != null">#{acdWaitTime},</if>
+            <if test="ttsText != null">#{ttsText},</if>
+            <if test="emptyNumberDetectionText != null and emptyNumberDetectionText != ''">#{emptyNumberDetectionText},</if>
+            <if test="intent != null">#{intent},</if>
+            <if test="asrSeconds != null">#{asrSeconds},</if>
+            <if test="ttsTimes != null">#{ttsTimes},</if>
+            <if test="ttsFlowTokens != null">#{ttsFlowTokens},</if>
+            <if test="inputTokens != null">#{inputTokens},</if>
+            <if test="outputTokens != null">#{outputTokens},</if>
+            <if test="totalCost != null">#{totalCost},</if>
+            <if test="billingStatus != null">#{billingStatus},</if>
+            <if test="callerNumber != null">#{callerNumber},</if>
+            <if test="ivrDtmfDigits != null and ivrDtmfDigits != ''">#{ivrDtmfDigits},</if>
+            <if test="manualAnsweredTime != null">#{manualAnsweredTime},</if>
+            <if test="manualAnsweredTimeLen != null">#{manualAnsweredTimeLen},</if>
+         </trim>
+    </insert>
+
+    <update id="updateAiSipCallPhone" parameterType="AiSipCallPhone">
+        update ai_sip_call_phone
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="batchId != null">batch_id = #{batchId},</if>
+            <if test="telephone != null and telephone != ''">telephone = #{telephone},</if>
+            <if test="custName != null and custName != ''">cust_name = #{custName},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="callstatus != null">callstatus = #{callstatus},</if>
+            <if test="calloutTime != null">callout_time = #{calloutTime},</if>
+            <if test="callcount != null">callcount = #{callcount},</if>
+            <if test="callEndTime != null">call_end_time = #{callEndTime},</if>
+            <if test="timeLen != null">time_len = #{timeLen},</if>
+            <if test="validTimeLen != null">valid_time_len = #{validTimeLen},</if>
+            <if test="uuid != null and uuid != ''">uuid = #{uuid},</if>
+            <if test="connectedTime != null">connected_time = #{connectedTime},</if>
+            <if test="hangupCause != null and hangupCause != ''">hangup_cause = #{hangupCause},</if>
+            <if test="answeredTime != null">answered_time = #{answeredTime},</if>
+            <if test="dialogue != null">dialogue = #{dialogue},</if>
+            <if test="wavfile != null and wavfile != ''">wavfile = #{wavfile},</if>
+            <if test="recordServerUrl != null and recordServerUrl != ''">record_server_url = #{recordServerUrl},</if>
+            <if test="bizJson != null and bizJson != ''">biz_json = #{bizJson},</if>
+            <if test="dialogueCount != null">dialogue_count = #{dialogueCount},</if>
+            <if test="acdOpnum != null and acdOpnum != ''">acd_opnum = #{acdOpnum},</if>
+            <if test="acdQueueTime != null">acd_queue_time = #{acdQueueTime},</if>
+            <if test="acdWaitTime != null">acd_wait_time = #{acdWaitTime},</if>
+            <if test="ttsText != null">tts_text = #{ttsText},</if>
+            <if test="emptyNumberDetectionText != null and emptyNumberDetectionText != ''">empty_number_detection_text = #{emptyNumberDetectionText},</if>
+            <if test="intent != null">intent = #{intent},</if>
+            <if test="asrSeconds != null">asr_seconds = #{asrSeconds},</if>
+            <if test="ttsTimes != null">tts_times = #{ttsTimes},</if>
+            <if test="ttsFlowTokens != null">tts_flow_tokens = #{ttsFlowTokens},</if>
+            <if test="inputTokens != null">input_tokens = #{inputTokens},</if>
+            <if test="outputTokens != null">output_tokens = #{outputTokens},</if>
+            <if test="totalCost != null">total_cost = #{totalCost},</if>
+            <if test="billingStatus != null">billing_status = #{billingStatus},</if>
+            <if test="callerNumber != null">caller_number = #{callerNumber},</if>
+            <if test="ivrDtmfDigits != null and ivrDtmfDigits != ''">ivr_dtmf_digits = #{ivrDtmfDigits},</if>
+            <if test="manualAnsweredTime != null">manual_answered_time = #{manualAnsweredTime},</if>
+            <if test="manualAnsweredTimeLen != null">manual_answered_time_len = #{manualAnsweredTimeLen},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteAiSipCallPhoneById" parameterType="String">
+        delete from ai_sip_call_phone where id = #{id}
+    </delete>
+
+    <delete id="deleteAiSipCallPhoneByIds" parameterType="String">
+        delete from ai_sip_call_phone where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="statByBatchId" parameterType="Long" resultType="com.fs.aiSipCall.dto.CallTaskStatModel">
+        SELECT batch_id AS batchId,
+               COUNT(1) AS phoneCount,
+               SUM(CASE WHEN callout_time > 0 THEN 1 ELSE 0 END) AS callCount,
+               SUM(CASE WHEN answered_time > 0 THEN 1 ELSE 0 END) AS connectCount
+        FROM ai_sip_call_phone
+        WHERE batch_id = #{batchId}
+    </select>
+</mapper>

+ 203 - 0
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallTaskMapper.xml

@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.aiSipCall.mapper.AiSipCallTaskMapper">
+    
+    <resultMap type="AiSipCallTask" id="AiSipCallTaskResult">
+        <result property="batchId"    column="batch_id"    />
+        <result property="groupId"    column="group_id"    />
+        <result property="batchName"    column="batch_name"    />
+        <result property="ifcall"    column="ifcall"    />
+        <result property="rate"    column="rate"    />
+        <result property="threadNum"    column="thread_num"    />
+        <result property="executing"    column="executing"    />
+        <result property="stopTime"    column="stop_time"    />
+        <result property="userid"    column="userid"    />
+        <result property="taskType"    column="task_type"    />
+        <result property="gatewayId"    column="gateway_id"    />
+        <result property="voiceCode"    column="voice_code"    />
+        <result property="voiceSource"    column="voice_source"    />
+        <result property="avgRingTimeLen"    column="avg_ring_time_len"    />
+        <result property="avgCallTalkTimeLen"    column="avg_call_talk_time_len"    />
+        <result property="avgCallEndProcessTimeLen"    column="avg_call_end_process_time_len"    />
+        <result property="callNodeNo"    column="call_node_no"    />
+        <result property="llmAccountId"    column="llm_account_id"    />
+        <result property="playTimes"    column="play_times"    />
+        <result property="asrProvider"    column="asr_provider"    />
+        <result property="aiTransferType"    column="ai_transfer_type"    />
+        <result property="aiTransferData"    column="ai_transfer_data"    />
+        <result property="autoStop"    column="auto_stop"    />
+        <result property="ivrId"    column="ivr_id"    />
+        <result property="remoteBatchId"    column="remote_batch_id"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+    </resultMap>
+
+    <sql id="selectAiSipCallTaskVo">
+        select * from ai_sip_call_task
+    </sql>
+
+    <select id="selectAiSipCallTaskList" parameterType="AiSipCallTask" resultMap="AiSipCallTaskResult">
+        <include refid="selectAiSipCallTaskVo"/>
+        <where>  
+            <if test="groupId != null  and groupId != ''"> and group_id = #{groupId}</if>
+            <if test="batchName != null  and batchName != ''"> and batch_name like concat('%', #{batchName}, '%')</if>
+            <if test="ifcall != null "> and ifcall = #{ifcall}</if>
+            <if test="rate != null "> and rate = #{rate}</if>
+            <if test="threadNum != null "> and thread_num = #{threadNum}</if>
+            <if test="executing != null "> and executing = #{executing}</if>
+            <if test="stopTime != null "> and stop_time = #{stopTime}</if>
+            <if test="userid != null  and userid != ''"> and userid = #{userid}</if>
+            <if test="taskType != null "> and task_type = #{taskType}</if>
+            <if test="gatewayId != null "> and gateway_id = #{gatewayId}</if>
+            <if test="voiceCode != null  and voiceCode != ''"> and voice_code = #{voiceCode}</if>
+            <if test="voiceSource != null  and voiceSource != ''"> and voice_source = #{voiceSource}</if>
+            <if test="avgRingTimeLen != null "> and avg_ring_time_len = #{avgRingTimeLen}</if>
+            <if test="avgCallTalkTimeLen != null "> and avg_call_talk_time_len = #{avgCallTalkTimeLen}</if>
+            <if test="avgCallEndProcessTimeLen != null "> and avg_call_end_process_time_len = #{avgCallEndProcessTimeLen}</if>
+            <if test="callNodeNo != null  and callNodeNo != ''"> and call_node_no = #{callNodeNo}</if>
+            <if test="llmAccountId != null "> and llm_account_id = #{llmAccountId}</if>
+            <if test="playTimes != null "> and play_times = #{playTimes}</if>
+            <if test="asrProvider != null  and asrProvider != ''"> and asr_provider = #{asrProvider}</if>
+            <if test="aiTransferType != null  and aiTransferType != ''"> and ai_transfer_type = #{aiTransferType}</if>
+            <if test="aiTransferData != null  and aiTransferData != ''"> and ai_transfer_data = #{aiTransferData}</if>
+            <if test="autoStop != null "> and auto_stop = #{autoStop}</if>
+            <if test="ivrId != null  and ivrId != ''"> and ivr_id = #{ivrId}</if>
+            <if test="remoteBatchId != null "> and remote_batch_id = #{remoteBatchId}</if>
+            <if test="createBy != null "> and create_by = #{createBy}</if>
+            <if test="createTime != null "> and create_time = #{createTime}</if>
+            <if test="updateBy != null "> and update_by = #{updateBy}</if>
+            <if test="updateTime != null "> and update_time = #{updateTime}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="createTimeStart != null  and createTimeStart != ''"> and create_time &gt;= #{createTimeStart}</if>
+            <if test="createTimeEnd != null  and createTimeEnd != ''"> and create_time &lt;= #{createTimeEnd}</if>
+        </where>
+    </select>
+    
+    <select id="selectAiSipCallTaskByBatchId" parameterType="Long" resultMap="AiSipCallTaskResult">
+        <include refid="selectAiSipCallTaskVo"/>
+        where batch_id = #{batchId}
+    </select>
+        
+    <insert id="insertAiSipCallTask" parameterType="AiSipCallTask" useGeneratedKeys="true" keyProperty="batchId">
+        insert into ai_sip_call_task
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="groupId != null and groupId != ''">group_id,</if>
+            <if test="batchName != null and batchName != ''">batch_name,</if>
+            <if test="ifcall != null">ifcall,</if>
+            <if test="rate != null">rate,</if>
+            <if test="threadNum != null">thread_num,</if>
+            <if test="executing != null">executing,</if>
+            <if test="stopTime != null">stop_time,</if>
+            <if test="userid != null and userid != ''">userid,</if>
+            <if test="taskType != null">task_type,</if>
+            <if test="gatewayId != null">gateway_id,</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code,</if>
+            <if test="voiceSource != null and voiceSource != ''">voice_source,</if>
+            <if test="avgRingTimeLen != null">avg_ring_time_len,</if>
+            <if test="avgCallTalkTimeLen != null">avg_call_talk_time_len,</if>
+            <if test="avgCallEndProcessTimeLen != null">avg_call_end_process_time_len,</if>
+            <if test="callNodeNo != null and callNodeNo != ''">call_node_no,</if>
+            <if test="llmAccountId != null">llm_account_id,</if>
+            <if test="playTimes != null">play_times,</if>
+            <if test="asrProvider != null">asr_provider,</if>
+            <if test="aiTransferType != null and aiTransferType != ''">ai_transfer_type,</if>
+            <if test="aiTransferData != null and aiTransferData != ''">ai_transfer_data,</if>
+            <if test="autoStop != null">auto_stop,</if>
+            <if test="ivrId != null">ivr_id,</if>
+            <if test="remoteBatchId != null">remote_batch_id,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="groupId != null and groupId != ''">#{groupId},</if>
+            <if test="batchName != null and batchName != ''">#{batchName},</if>
+            <if test="ifcall != null">#{ifcall},</if>
+            <if test="rate != null">#{rate},</if>
+            <if test="threadNum != null">#{threadNum},</if>
+            <if test="executing != null">#{executing},</if>
+            <if test="stopTime != null">#{stopTime},</if>
+            <if test="userid != null and userid != ''">#{userid},</if>
+            <if test="taskType != null">#{taskType},</if>
+            <if test="gatewayId != null">#{gatewayId},</if>
+            <if test="voiceCode != null and voiceCode != ''">#{voiceCode},</if>
+            <if test="voiceSource != null and voiceSource != ''">#{voiceSource},</if>
+            <if test="avgRingTimeLen != null">#{avgRingTimeLen},</if>
+            <if test="avgCallTalkTimeLen != null">#{avgCallTalkTimeLen},</if>
+            <if test="avgCallEndProcessTimeLen != null">#{avgCallEndProcessTimeLen},</if>
+            <if test="callNodeNo != null and callNodeNo != ''">#{callNodeNo},</if>
+            <if test="llmAccountId != null">#{llmAccountId},</if>
+            <if test="playTimes != null">#{playTimes},</if>
+            <if test="asrProvider != null">#{asrProvider},</if>
+            <if test="aiTransferType != null and aiTransferType != ''">#{aiTransferType},</if>
+            <if test="aiTransferData != null and aiTransferData != ''">#{aiTransferData},</if>
+            <if test="autoStop != null">#{autoStop},</if>
+            <if test="ivrId != null">#{ivrId},</if>
+            <if test="remoteBatchId != null">#{remoteBatchId},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateAiSipCallTask" parameterType="AiSipCallTask">
+        update ai_sip_call_task
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="groupId != null and groupId != ''">group_id = #{groupId},</if>
+            <if test="batchName != null and batchName != ''">batch_name = #{batchName},</if>
+            <if test="ifcall != null">ifcall = #{ifcall},</if>
+            <if test="rate != null">rate = #{rate},</if>
+            <if test="threadNum != null">thread_num = #{threadNum},</if>
+            <if test="executing != null">executing = #{executing},</if>
+            <if test="stopTime != null">stop_time = #{stopTime},</if>
+            <if test="userid != null and userid != ''">userid = #{userid},</if>
+            <if test="taskType != null">task_type = #{taskType},</if>
+            <if test="gatewayId != null">gateway_id = #{gatewayId},</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code = #{voiceCode},</if>
+            <if test="voiceSource != null and voiceSource != ''">voice_source = #{voiceSource},</if>
+            <if test="avgRingTimeLen != null">avg_ring_time_len = #{avgRingTimeLen},</if>
+            <if test="avgCallTalkTimeLen != null">avg_call_talk_time_len = #{avgCallTalkTimeLen},</if>
+            <if test="avgCallEndProcessTimeLen != null">avg_call_end_process_time_len = #{avgCallEndProcessTimeLen},</if>
+            <if test="callNodeNo != null and callNodeNo != ''">call_node_no = #{callNodeNo},</if>
+            <if test="llmAccountId != null">llm_account_id = #{llmAccountId},</if>
+            <if test="playTimes != null">play_times = #{playTimes},</if>
+            <if test="asrProvider != null">asr_provider = #{asrProvider},</if>
+            <if test="aiTransferType != null and aiTransferType != ''">ai_transfer_type = #{aiTransferType},</if>
+            <if test="aiTransferData != null and aiTransferData != ''">ai_transfer_data = #{aiTransferData},</if>
+            <if test="autoStop != null">auto_stop = #{autoStop},</if>
+            <if test="ivrId != null">ivr_id = #{ivrId},</if>
+            <if test="remoteBatchId != null">remote_batch_id = #{remoteBatchId},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+        </trim>
+        where batch_id = #{batchId}
+    </update>
+
+    <delete id="deleteAiSipCallTaskByBatchId" parameterType="Long">
+        delete from ai_sip_call_task where batch_id = #{batchId}
+    </delete>
+
+    <delete id="deleteAiSipCallTaskByBatchIds" parameterType="String">
+        delete from ai_sip_call_task where batch_id in 
+        <foreach item="batchId" collection="array" open="(" separator="," close=")">
+            #{batchId}
+        </foreach>
+    </delete>
+</mapper>

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

@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.aiSipCall.mapper.AiSipCallUserMapper">
+    
+    <resultMap type="AiSipCallUser" id="AiSipCallUserResult">
+        <result property="userId"    column="user_id"    />
+        <result property="deptId"    column="dept_id"    />
+        <result property="loginName"    column="login_name"    />
+        <result property="userName"    column="user_name"    />
+        <result property="userType"    column="user_type"    />
+        <result property="email"    column="email"    />
+        <result property="phonenumber"    column="phonenumber"    />
+        <result property="sex"    column="sex"    />
+        <result property="avatar"    column="avatar"    />
+        <result property="password"    column="password"    />
+        <result property="salt"    column="salt"    />
+        <result property="status"    column="status"    />
+        <result property="delFlag"    column="del_flag"    />
+        <result property="loginIp"    column="login_ip"    />
+        <result property="loginDate"    column="login_date"    />
+        <result property="pwdUpdateDate"    column="pwd_update_date"    />
+        <result property="remark"    column="remark"    />
+        <result property="logo"    column="logo"    />
+        <result property="extNum"    column="ext_num"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+    </resultMap>
+
+    <sql id="selectAiSipCallUserVo">
+        select user_id, dept_id, login_name, user_name, user_type, email, phonenumber, sex, avatar, password, salt, status, del_flag, login_ip, login_date, pwd_update_date, remark, logo, ext_num, create_by, create_time, update_by, update_time, company_id,company_user_id from ai_sip_call_user
+    </sql>
+
+    <select id="selectAiSipCallUserList" parameterType="AiSipCallUser" resultMap="AiSipCallUserResult">
+        <include refid="selectAiSipCallUserVo"/>
+        <where>  
+            <if test="deptId != null "> and dept_id = #{deptId}</if>
+            <if test="loginName != null  and loginName != ''"> and login_name like concat('%', #{loginName}, '%')</if>
+            <if test="userName != null  and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
+            <if test="userType != null  and userType != ''"> and user_type = #{userType}</if>
+            <if test="email != null  and email != ''"> and email = #{email}</if>
+            <if test="phonenumber != null  and phonenumber != ''"> and phonenumber = #{phonenumber}</if>
+            <if test="sex != null  and sex != ''"> and sex = #{sex}</if>
+            <if test="avatar != null  and avatar != ''"> and avatar = #{avatar}</if>
+            <if test="password != null  and password != ''"> and password = #{password}</if>
+            <if test="salt != null  and salt != ''"> and salt = #{salt}</if>
+            <if test="status != null  and status != ''"> and status = #{status}</if>
+            <if test="loginIp != null  and loginIp != ''"> and login_ip = #{loginIp}</if>
+            <if test="loginDate != null "> and login_date = #{loginDate}</if>
+            <if test="pwdUpdateDate != null "> and pwd_update_date = #{pwdUpdateDate}</if>
+            <if test="logo != null  and logo != ''"> and logo = #{logo}</if>
+            <if test="extNum != null  and extNum != ''"> and ext_num = #{extNum}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+        </where>
+    </select>
+    
+    <select id="selectAiSipCallUserByUserId" parameterType="Long" resultMap="AiSipCallUserResult">
+        <include refid="selectAiSipCallUserVo"/>
+        where user_id = #{userId}
+    </select>
+        
+    <insert id="insertAiSipCallUser" parameterType="AiSipCallUser">
+        insert into ai_sip_call_user
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userId != null">user_id,</if>
+            <if test="deptId != null">dept_id,</if>
+            <if test="loginName != null and loginName != ''">login_name,</if>
+            <if test="userName != null">user_name,</if>
+            <if test="userType != null">user_type,</if>
+            <if test="email != null">email,</if>
+            <if test="phonenumber != null">phonenumber,</if>
+            <if test="sex != null">sex,</if>
+            <if test="avatar != null">avatar,</if>
+            <if test="password != null">password,</if>
+            <if test="salt != null">salt,</if>
+            <if test="status != null">status,</if>
+            <if test="delFlag != null">del_flag,</if>
+            <if test="loginIp != null">login_ip,</if>
+            <if test="loginDate != null">login_date,</if>
+            <if test="pwdUpdateDate != null">pwd_update_date,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="remark != null">remark,</if>
+            <if test="logo != null">logo,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="extNum != null and extNum != ''">ext_num,</if>
+            <if test="companyId != null ">company_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userId != null">#{userId},</if>
+            <if test="deptId != null">#{deptId},</if>
+            <if test="loginName != null and loginName != ''">#{loginName},</if>
+            <if test="userName != null">#{userName},</if>
+            <if test="userType != null">#{userType},</if>
+            <if test="email != null">#{email},</if>
+            <if test="phonenumber != null">#{phonenumber},</if>
+            <if test="sex != null">#{sex},</if>
+            <if test="avatar != null">#{avatar},</if>
+            <if test="password != null">#{password},</if>
+            <if test="salt != null">#{salt},</if>
+            <if test="status != null">#{status},</if>
+            <if test="delFlag != null">#{delFlag},</if>
+            <if test="loginIp != null">#{loginIp},</if>
+            <if test="loginDate != null">#{loginDate},</if>
+            <if test="pwdUpdateDate != null">#{pwdUpdateDate},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="logo != null">#{logo},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="extNum != null and extNum != ''">#{extNum},</if>
+            <if test="companyId != null">#{companyId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateAiSipCallUser" parameterType="AiSipCallUser">
+        update ai_sip_call_user
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="deptId != null">dept_id = #{deptId},</if>
+            <if test="loginName != null and loginName != ''">login_name = #{loginName},</if>
+            <if test="userName != null">user_name = #{userName},</if>
+            <if test="userType != null">user_type = #{userType},</if>
+            <if test="email != null">email = #{email},</if>
+            <if test="phonenumber != null">phonenumber = #{phonenumber},</if>
+            <if test="sex != null">sex = #{sex},</if>
+            <if test="avatar != null">avatar = #{avatar},</if>
+            <if test="password != null">password = #{password},</if>
+            <if test="salt != null">salt = #{salt},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="delFlag != null">del_flag = #{delFlag},</if>
+            <if test="loginIp != null">login_ip = #{loginIp},</if>
+            <if test="loginDate != null">login_date = #{loginDate},</if>
+            <if test="pwdUpdateDate != null">pwd_update_date = #{pwdUpdateDate},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="logo != null">logo = #{logo},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="extNum != null and extNum != ''">ext_num = #{extNum},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+        </trim>
+        where user_id = #{userId}
+    </update>
+
+    <delete id="deleteAiSipCallUserByUserId" parameterType="Long">
+        delete from ai_sip_call_user where user_id = #{userId}
+    </delete>
+
+    <delete id="deleteAiSipCallUserByUserIds" parameterType="String">
+        delete from ai_sip_call_user where user_id in 
+        <foreach item="userId" collection="array" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+    </delete>
+</mapper>

+ 86 - 0
fs-service/src/main/resources/mapper/aiSipCall/AiSipCallVoiceTtsAliyunMapper.xml

@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.aiSipCall.mapper.AiSipCallVoiceTtsAliyunMapper">
+    
+    <resultMap type="AiSipCallVoiceTtsAliyun" id="AiSipCallVoiceTtsAliyunResult">
+        <result property="id"    column="id"    />
+        <result property="voiceName"    column="voice_name"    />
+        <result property="voiceCode"    column="voice_code"    />
+        <result property="voiceEnabled"    column="voice_enabled"    />
+        <result property="voiceSource"    column="voice_source"    />
+        <result property="priority"    column="priority"    />
+        <result property="provider"    column="provider"    />
+        <result property="remoteVoiceTtsAliyunId"    column="remote_voice_tts_aliyun_id"    />
+    </resultMap>
+
+    <sql id="selectAiSipCallVoiceTtsAliyunVo">
+        select id, voice_name, voice_code, voice_enabled, voice_source, priority, provider, remote_voice_tts_aliyun_id from ai_sip_call_voice_tts_aliyun
+    </sql>
+
+    <select id="selectAiSipCallVoiceTtsAliyunList" parameterType="AiSipCallVoiceTtsAliyun" resultMap="AiSipCallVoiceTtsAliyunResult">
+        <include refid="selectAiSipCallVoiceTtsAliyunVo"/>
+        <where>  
+            <if test="voiceName != null  and voiceName != ''"> and voice_name like concat('%', #{voiceName}, '%')</if>
+            <if test="voiceCode != null  and voiceCode != ''"> and voice_code = #{voiceCode}</if>
+            <if test="voiceEnabled != null "> and voice_enabled = #{voiceEnabled}</if>
+            <if test="voiceSource != null  and voiceSource != ''"> and voice_source = #{voiceSource}</if>
+            <if test="priority != null "> and priority = #{priority}</if>
+            <if test="provider != null  and provider != ''"> and provider = #{provider}</if>
+            <if test="remoteVoiceTtsAliyunId != null "> and remote_voice_tts_aliyun_id = #{remoteVoiceTtsAliyunId}</if>
+        </where>
+    </select>
+    
+    <select id="selectAiSipCallVoiceTtsAliyunById" parameterType="Long" resultMap="AiSipCallVoiceTtsAliyunResult">
+        <include refid="selectAiSipCallVoiceTtsAliyunVo"/>
+        where id = #{id}
+    </select>
+        
+    <insert id="insertAiSipCallVoiceTtsAliyun" parameterType="AiSipCallVoiceTtsAliyun" useGeneratedKeys="true" keyProperty="id">
+        insert into ai_sip_call_voice_tts_aliyun
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="voiceName != null and voiceName != ''">voice_name,</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code,</if>
+            <if test="voiceEnabled != null">voice_enabled,</if>
+            <if test="voiceSource != null">voice_source,</if>
+            <if test="priority != null">priority,</if>
+            <if test="provider != null">provider,</if>
+            <if test="remoteVoiceTtsAliyunId != null">remote_voice_tts_aliyun_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="voiceName != null and voiceName != ''">#{voiceName},</if>
+            <if test="voiceCode != null and voiceCode != ''">#{voiceCode},</if>
+            <if test="voiceEnabled != null">#{voiceEnabled},</if>
+            <if test="voiceSource != null">#{voiceSource},</if>
+            <if test="priority != null">#{priority},</if>
+            <if test="provider != null">#{provider},</if>
+            <if test="remoteVoiceTtsAliyunId != null">#{remoteVoiceTtsAliyunId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateAiSipCallVoiceTtsAliyun" parameterType="AiSipCallVoiceTtsAliyun">
+        update ai_sip_call_voice_tts_aliyun
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="voiceName != null and voiceName != ''">voice_name = #{voiceName},</if>
+            <if test="voiceCode != null and voiceCode != ''">voice_code = #{voiceCode},</if>
+            <if test="voiceEnabled != null">voice_enabled = #{voiceEnabled},</if>
+            <if test="voiceSource != null">voice_source = #{voiceSource},</if>
+            <if test="priority != null">priority = #{priority},</if>
+            <if test="provider != null">provider = #{provider},</if>
+            <if test="remoteVoiceTtsAliyunId != null">remote_voice_tts_aliyun_id = #{remoteVoiceTtsAliyunId},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteAiSipCallVoiceTtsAliyunById" parameterType="Long">
+        delete from ai_sip_call_voice_tts_aliyun where id = #{id}
+    </delete>
+
+    <delete id="deleteAiSipCallVoiceTtsAliyunByIds" parameterType="String">
+        delete from ai_sip_call_voice_tts_aliyun where id in 
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 2 - 1
fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

@@ -102,7 +102,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         u.doctor_id,
         u.cid_server_id,
         d.dept_name,
-        d.leader
+        d.leader,
+        u.ai_sip_call_user_id
         from
         company_user u
         left join