Explorar el Código

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

# Conflicts:
#	fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
#	fs-service/src/main/resources/mapper/his/FsUserMapper.xml
caoliqin hace 2 semanas
padre
commit
daa2966676
Se han modificado 100 ficheros con 9951 adiciones y 141 borrados
  1. 102 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyVoiceRoboticCallBlacklistController.java
  2. 0 1
      fs-admin/src/main/java/com/fs/course/business/FsVideoResourceBusinessService.java
  3. 5 1
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  4. 92 0
      fs-admin/src/main/java/com/fs/his/controller/EasyCallController.java
  5. 34 3
      fs-admin/src/main/java/com/fs/his/task/Task.java
  6. 80 70
      fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java
  7. 1 1
      fs-ai-call-task/src/main/java/com/fs/app/task/Task.java
  8. 27 4
      fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  9. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallBizGroupController.java
  10. 101 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallGatewayController.java
  11. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallLlmAgentAccountController.java
  12. 167 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallOutboundCdrController.java
  13. 163 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallPhoneController.java
  14. 257 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallTaskController.java
  15. 152 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallUserController.java
  16. 97 0
      fs-company/src/main/java/com/fs/company/controller/aiSipCall/AiSipCallVoiceTtsAliyunController.java
  17. 24 0
      fs-company/src/main/java/com/fs/company/controller/aicall/CcLlmAgentAccountController.java
  18. 59 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceCloneController.java
  19. 113 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallBlacklistController.java
  20. 27 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java
  21. 96 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java
  22. 28 2
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerAnalyzeController.java
  23. 0 7
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  24. 107 0
      fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatMessageController.java
  25. 156 0
      fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatSessionController.java
  26. 65 0
      fs-company/src/main/java/com/fs/company/controller/im/FsImMsgSendLogController.java
  27. 10 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  28. 192 0
      fs-company/src/main/java/com/fs/company/controller/statistic/courseStatisticController.java
  29. 158 39
      fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java
  30. 24 2
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  31. 1 1
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  32. 134 0
      fs-service/src/main/java/com/fs/aiSipCall/RemoteCommon.java
  33. 38 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallBizGroup.java
  34. 85 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallGateway.java
  35. 70 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallLlmAgentAccount.java
  36. 97 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallOutboundCdr.java
  37. 270 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallPhone.java
  38. 164 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallTask.java
  39. 99 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallUser.java
  40. 50 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/AiSipCallVoiceTtsAliyun.java
  41. 55 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/CcCustCallRecord.java
  42. 86 0
      fs-service/src/main/java/com/fs/aiSipCall/domain/CcCustInfo.java
  43. 16 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/AiCallListModel.java
  44. 24 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CallTaskStatModel.java
  45. 14 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CommonCallListModel.java
  46. 13 0
      fs-service/src/main/java/com/fs/aiSipCall/dto/CommonPhoneModel.java
  47. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallBizGroupMapper.java
  48. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallGatewayMapper.java
  49. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallLlmAgentAccountMapper.java
  50. 71 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallOutboundCdrMapper.java
  51. 96 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallPhoneMapper.java
  52. 67 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallTaskMapper.java
  53. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallUserMapper.java
  54. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/mapper/AiSipCallVoiceTtsAliyunMapper.java
  55. 31 0
      fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordByUuidQueryParams.java
  56. 36 0
      fs-service/src/main/java/com/fs/aiSipCall/param/ApiCallRecordQueryParams.java
  57. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallBizGroupService.java
  58. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallGatewayService.java
  59. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallLlmAgentAccountService.java
  60. 74 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallOutboundCdrService.java
  61. 84 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallPhoneService.java
  62. 74 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallTaskService.java
  63. 72 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallUserService.java
  64. 62 0
      fs-service/src/main/java/com/fs/aiSipCall/service/IAiSipCallVoiceTtsAliyunService.java
  65. 110 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallBizGroupServiceImpl.java
  66. 112 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallGatewayServiceImpl.java
  67. 108 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallLlmAgentAccountServiceImpl.java
  68. 520 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallOutboundCdrServiceImpl.java
  69. 429 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallPhoneServiceImpl.java
  70. 278 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallTaskServiceImpl.java
  71. 183 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallUserServiceImpl.java
  72. 108 0
      fs-service/src/main/java/com/fs/aiSipCall/service/impl/AiSipCallVoiceTtsAliyunServiceImpl.java
  73. 85 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/CharsetKit.java
  74. 1010 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/Convert.java
  75. 239 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/DateUtils.java
  76. 90 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/StrFormatter.java
  77. 669 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/StringUtils.java
  78. 34 0
      fs-service/src/main/java/com/fs/aiSipCall/utils/UuidGenerator.java
  79. 59 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/ApiCallRecordQueryVo.java
  80. 45 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/CallPhoneExportVo.java
  81. 30 0
      fs-service/src/main/java/com/fs/aiSipCall/vo/CcExtNumVo.java
  82. 38 0
      fs-service/src/main/java/com/fs/aicall/domain/CcTtsAliyun.java
  83. 57 0
      fs-service/src/main/java/com/fs/aicall/mapper/CcTtsAliyunMapper.java
  84. 48 0
      fs-service/src/main/java/com/fs/aicall/service/ICcTtsAliyunService.java
  85. 56 0
      fs-service/src/main/java/com/fs/aicall/service/impl/CcTtsAliyunServiceImpl.java
  86. 12 0
      fs-service/src/main/java/com/fs/app/service/AppPayService.java
  87. 63 0
      fs-service/src/main/java/com/fs/app/service/impl/AppPayServiceImpl.java
  88. 64 0
      fs-service/src/main/java/com/fs/app/service/param/FsImMsgSendLogRequest.java
  89. 78 0
      fs-service/src/main/java/com/fs/app/service/param/FsImMsgSendLogResponse.java
  90. 20 0
      fs-service/src/main/java/com/fs/app/service/param/FsImMsgSendLogStatisticsResponse.java
  91. 44 9
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  92. 3 0
      fs-service/src/main/java/com/fs/company/domain/Company.java
  93. 22 0
      fs-service/src/main/java/com/fs/company/domain/CompanyBindGateway.java
  94. 37 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallBlackLog.java
  95. 85 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallBlacklist.java
  96. 3 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java
  97. 58 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowEdgeVersion.java
  98. 67 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowNodeVersion.java
  99. 94 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowVersion.java
  100. 83 0
      fs-service/src/main/java/com/fs/company/enums/BusinessTypeEnum.java

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

@@ -0,0 +1,102 @@
+package com.fs.company.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRoboticCallBlacklist;
+import com.fs.company.service.ICompanyVoiceRoboticCallBlacklistService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 黑名单Controller
+ *
+ * @author fs
+ * @date 2023-02-23
+ */
+@RestController
+@RequestMapping("/company/companyVoiceRoboticCallBlacklist")
+public class CompanyVoiceRoboticCallBlacklistController extends BaseController
+{
+    @Autowired
+    private ICompanyVoiceRoboticCallBlacklistService companyVoiceRoboticCallBlacklistService;
+
+    /**
+     * 查询黑名单列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallBlacklist> list = companyVoiceRoboticCallBlacklistService.selectCompanyVoiceRoboticCallBlacklistList(companyVoiceRoboticCallBlacklist);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出黑名单列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:export')")
+    @Log(title = "黑名单", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
+    {
+        List<CompanyVoiceRoboticCallBlacklist> list = companyVoiceRoboticCallBlacklistService.selectCompanyVoiceRoboticCallBlacklistList(companyVoiceRoboticCallBlacklist);
+        ExcelUtil<CompanyVoiceRoboticCallBlacklist> util = new ExcelUtil<CompanyVoiceRoboticCallBlacklist>(CompanyVoiceRoboticCallBlacklist.class);
+        return util.exportExcel(list, "companyVoiceBlacklist");
+    }
+
+    /**
+     * 获取黑名单详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:query')")
+    @GetMapping(value = "/{blacklistId}")
+    public AjaxResult getInfo(@PathVariable("blacklistId") Long blacklistId)
+    {
+        return AjaxResult.success(companyVoiceRoboticCallBlacklistService.selectCompanyVoiceRoboticCallBlacklistById(blacklistId));
+    }
+
+    /**
+     * 新增黑名单
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:add')")
+    @Log(title = "黑名单", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
+    {
+        return toAjax(companyVoiceRoboticCallBlacklistService.insertCompanyVoiceRoboticCallBlacklist(companyVoiceRoboticCallBlacklist));
+    }
+
+    /**
+     * 修改黑名单nessType = BusinessType.UPDATE)
+     * */
+    @PutMapping
+    @Log(title = "修改黑名单信息", businessType = BusinessType.INSERT)
+    public AjaxResult edit(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist)
+    {
+        return toAjax(companyVoiceRoboticCallBlacklistService.updateCompanyVoiceRoboticCallBlacklist(companyVoiceRoboticCallBlacklist));
+    }
+
+    /**
+     * 删除黑名单
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceBlacklist:remove')")
+    @Log(title = "黑名单", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{blacklistIds}")
+    public AjaxResult remove(@PathVariable Long[] blacklistIds)
+    {
+        return toAjax(companyVoiceRoboticCallBlacklistService.deleteCompanyVoiceRoboticCallBlacklistByIds(blacklistIds));
+    }
+
+    @Log(title = "修改黑名单状态", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody CompanyVoiceRoboticCallBlacklist companyVoiceRoboticCallBlacklist) {
+        return toAjax(companyVoiceRoboticCallBlacklistService.changeStatus(companyVoiceRoboticCallBlacklist));
+    }
+}

+ 0 - 1
fs-admin/src/main/java/com/fs/course/business/FsVideoResourceBusinessService.java

@@ -36,7 +36,6 @@ public class FsVideoResourceBusinessService {
                 .set("file_key", fsVideoResource.getFileKey())
                 .set("file_name", fsVideoResource.getFileName())
                 .set("thumbnail", fsVideoResource.getThumbnail())
-                .set("display_type", fsVideoResource.getDisplayType())
         );
     }
 }

+ 5 - 1
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -5,6 +5,7 @@ import java.util.List;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.course.param.FsCourseOverParam;
+import com.fs.course.param.FsCourseSummaryDetailQueryParam;
 import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
@@ -249,7 +250,10 @@ public class FsCourseWatchLogController extends BaseController
         if (videoId == null || periodId == null) {
             return R.error("视频ID和营期ID不能为空");
         }
-        return R.ok().put("data", fsCourseWatchLogService.getCourseStatisticsDetail(videoId, periodId));
+        FsCourseSummaryDetailQueryParam param = new FsCourseSummaryDetailQueryParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        return R.ok().put("data", fsCourseWatchLogService.getCourseStatisticsDetail(param));
     }
 
     /**

+ 92 - 0
fs-admin/src/main/java/com/fs/his/controller/EasyCallController.java

@@ -0,0 +1,92 @@
+package com.fs.his.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.service.easycall.IEasyCallService;
+import com.fs.company.vo.easycall.*;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/6 14:28
+ * @description EasyCallCenter365 外呼管理控制器
+ * <p>
+ * 所有接口均需要登录态,通过 TokenService 获取当前登录用户的 companyId。
+ * 接口地址前缀:/company/easyCall
+ * 对应三方服务器:http://129.28.164.235:8899
+ */
+@Api(tags = "EasyCallCenter365外呼管理")
+@RestController
+@RequestMapping("/easyCall")
+public class EasyCallController extends BaseController {
+
+    @Autowired
+    private IEasyCallService easyCallService;
+    @Autowired
+    ISysConfigService iSysConfigService;
+
+    /**
+     * 获取外呼网关列表
+     * 网关是外呼连路的入口,创建任务时需要选择对应的网关 ID
+     */
+    @ApiOperation("获取网关列表")
+    @GetMapping("/gateway/list")
+    public R getGatewayList() {
+        List<EasyCallGatewayVO> list = easyCallService.getGatewayList(null);
+        return R.ok().put("data", list);
+    }
+
+    /**
+     * 获取外呼网关列表
+     * 网关是外呼连路的入口,创建任务时需要选择对应的网关 ID
+     */
+    @ApiOperation("获取网关列表")
+    @GetMapping("/gateway/getGatewayCompanyList")
+    public R getGatewayCompanyList() {
+        List<EasyCallGatewayVO> resList = new ArrayList<>();
+        List<EasyCallGatewayVO> list = easyCallService.getGatewayList(null);
+        // 如果原始列表为空,直接返回空列表
+        if (list == null || list.isEmpty()) {
+            return R.ok().put("data", resList);
+        }
+        SysConfig cidConf = iSysConfigService.selectConfigByConfigKey("cId.config");
+        if (null != cidConf && StringUtils.isNotBlank(cidConf.getConfigValue())) {
+            JSONObject jo = JSONObject.parseObject(cidConf.getConfigValue());
+            if (null != jo && jo.containsKey("showGatewayIds")) {
+                List<Long> gatewayIdList = jo.getJSONArray("showGatewayIds").toJavaList(Long.class);
+                if (gatewayIdList != null && !gatewayIdList.isEmpty()) {
+                    // 将配置中的网关ID列表转换为Set集合便于快速匹配
+                    Set<Long> gatewayIdSet = gatewayIdList.stream()
+                            .filter(id -> id != null)
+                            .collect(Collectors.toSet());
+                    // 过滤list,只保留id在配置集合中的网关
+                    resList = list.stream()
+                            .filter(gateway -> gateway.getId() != null && gatewayIdSet.contains(gateway.getId()))
+                            .collect(Collectors.toList());
+                } else {
+                    // 配置为空时返回原始列表
+                    resList = list;
+                }
+            } else {
+                // 未配置showGatewayIds时返回原始列表
+                resList = list;
+            }
+        } else {
+            // 未找到配置时返回原始列表
+            resList = list;
+        }
+        return R.ok().put("data", resList);
+    }
+}

+ 34 - 3
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -1778,13 +1778,44 @@ public class Task {
             return;
         }
         for (Map.Entry<String, BatchSendCourseAllDTO> entry : toSendMap) {
+            String key=entry.getKey();
             //执行发送消息任务
             BatchSendCourseAllDTO batchSendCourseAllDTO = entry.getValue();
-            openIMService.batchSendCourseTask(batchSendCourseAllDTO.getBatchSendCourseDTO(), batchSendCourseAllDTO.getOpenImBatchMsgDTO(), batchSendCourseAllDTO.getProject(), batchSendCourseAllDTO.getImMsgSendDetailList());
+            if (batchSendCourseAllDTO == null) {
+                logger.error("batchSendCourseAllDTO 为 null,key: {}", key);
+                redisTemplate.opsForHash().delete(redisKey, key);
+                continue; // 跳过当前循环
+            }
+            OpenImBatchMsgDTO openImBatchMsgDTO = batchSendCourseAllDTO.getOpenImBatchMsgDTO();
+            Integer nowCount=openImBatchMsgDTO.getCount();
+            if (nowCount == null) {
+                nowCount = 0;
+            }
+            OpenImResponseDTO responseDTO=new OpenImResponseDTO();
+            try {
+                responseDTO=  openIMService.batchSendCourseTask(batchSendCourseAllDTO.getBatchSendCourseDTO(), batchSendCourseAllDTO.getOpenImBatchMsgDTO(), batchSendCourseAllDTO.getProject(), batchSendCourseAllDTO.getImMsgSendDetailList());
+            } catch (Exception e) {
+                responseDTO.setErrCode(500);
+                responseDTO.setErrMsg(e.getMessage());
+                e.printStackTrace();
+            }
 
-            // 执行结束,删除
-            this.redisTemplate.<String, BatchSendCourseAllDTO>opsForHash().delete(redisKey, entry.getKey());
 
+            if(nowCount>1){ // 重试一次后放弃
+                logger.error("im会员定时发课重试三次后放弃,key{}", key);
+                redisTemplate.opsForHash().delete(redisKey, key);
+                continue;
+            }
+
+            if(responseDTO!=null && responseDTO.getErrCode() == 0){
+                // 执行结束,删除
+                redisTemplate.opsForHash().delete(redisKey, key);
+            }else {
+                openImBatchMsgDTO.setCount(nowCount + 1);// 次数加一
+                batchSendCourseAllDTO.setOpenImBatchMsgDTO(openImBatchMsgDTO);
+                // 错误更新次数 重新放入redis中
+                redisTemplate.opsForHash().put(redisKey, key, batchSendCourseAllDTO);
+            }
         }
 
     }

+ 80 - 70
fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java

@@ -1,7 +1,7 @@
 package com.fs.task;
 
+import com.fs.crm.domain.CrmCustomerAnalyze;
 import com.fs.crm.service.ICrmCustomerAnalyzeService;
-import com.google.common.collect.Lists;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -11,7 +11,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 
 @Component("CrmCustomerAiProcessingTask")
 @RequiredArgsConstructor
@@ -21,6 +20,7 @@ public class CrmCustomerAiProcessingTask {
     private final RedisTemplate redisTemplate;
 
     private static final String CRM_AI_REDIS_KEY = "crm:AI:data:processing";
+
     private final ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
 
     // 自定义线程池
@@ -37,58 +37,49 @@ public class CrmCustomerAiProcessingTask {
             new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程处理
     );
 
+    @SuppressWarnings("unchecked")
     public void process() {
-        List<Map<String,String>> range = (List<Map<String, String>>) redisTemplate.opsForList().range(CRM_AI_REDIS_KEY, 0, -1);
+//        一次只处理5条,AI响应慢避免阻塞
+        List<Map<String, Object>> range =
+                (List<Map<String, Object>>) redisTemplate.opsForList().range(CRM_AI_REDIS_KEY, 0, 4);
         if (range == null || range.isEmpty()) {
             log.info("CrmCustomerAiProcessingTask没有待处理的数据");
             return;
         }
-        log.info("CrmCustomerAiProcessingTask开始处理数据,条数"+range.size());
-        // 2. 每100条分成一批
-        List<List<Map<String, String>>> partitions = Lists.partition(range, 10);//ai沟通很慢,批量处理10条每批
-        int totalBatches = partitions.size();
-        log.info("共分为 {} 批, 每批"+(partitions.size()>1?"10":range.size())+"条", totalBatches);
+        final int total = range.size();
+        log.info("CrmCustomerAiProcessingTask开始处理数据, 条数: {}", total);
+
 
-        // 3. 统计计数器
         AtomicInteger successCount = new AtomicInteger(0);
         AtomicInteger failCount = new AtomicInteger(0);
-
         long startTime = System.currentTimeMillis();
-
-        // 4. 多线程处理
-        List<CompletableFuture<Void>> futures = partitions.stream()
-                .map(batch -> CompletableFuture.runAsync(() -> {
-                    processBatch(batch, successCount, failCount);
-                }, executorService))
-                .collect(Collectors.toList());
-
-        // 5. 等待所有任务完成
+        CompletableFuture<Void> futures = CompletableFuture.runAsync(
+                () -> processBatch(range, successCount, failCount), executorService);
         try {
-            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+            CompletableFuture.allOf(futures).join();
+        } catch (CompletionException e) {
+            Throwable cause = e.getCause() != null ? e.getCause() : e;
+            log.error("多线程处理异常", cause);
+            return;
+        }
 
-            long costTime = System.currentTimeMillis() - startTime;
-            log.info("CrmCustomerAiProcessingTask处理完成, 总条数: {}, 成功: {}, 失败: {}, 耗时: {}ms",
-                    range.size(), successCount.get(), failCount.get(), costTime);
-
-            // 6. 处理完成后,从Redis中删除已处理的数据
-            if (failCount.get() == 0) {
-                // 全部成功,删除整个key
-                redisTemplate.delete(CRM_AI_REDIS_KEY);
-                log.info("全部处理成功,已删除Redis数据");
-            } else {
-                // 有失败的数据,保留或移到失败队列
-                handleFailedData(partitions, successCount.get());
-            }
+        long costTime = System.currentTimeMillis() - startTime;
+        log.info("CrmCustomerAiProcessingTask处理完成, 总条数: {}, 成功: {}, 失败: {}, 耗时: {}ms",
+                total, successCount.get(), failCount.get(), costTime);
 
-        } catch (Exception e) {
-            log.error("多线程处理异常", e);
+        // 当前 processBatch 内任一条失败会抛异常导致 join 失败;能走到此处说明整批成功
+        if (failCount.get() == 0 && successCount.get() == total) {
+            redisTemplate.delete(CRM_AI_REDIS_KEY);
+            log.info("全部处理成功,已删除Redis数据");
+            return;
         }
-
+        log.warn("计数与预期不一致: 总条数={}, 成功={}, 失败={}, 未删除 Redis 队列", total,
+                successCount.get(), failCount.get());
     }
     /**
      * 处理单个批次
      */
-    private void processBatch(List<Map<String, String>> batch,
+    private void processBatch(List<Map<String, Object>> batch,
                               AtomicInteger successCount,
                               AtomicInteger failCount) {
         String threadName = Thread.currentThread().getName();
@@ -97,25 +88,13 @@ public class CrmCustomerAiProcessingTask {
         try {
             log.info("线程 {} 开始处理批次, 数据量: {}", threadName, batch.size());
 
-            // 示例:处理每条数据
-            for (Map<String, String> data : batch) {
-                // 获取数据
-                String customerId = data.get("customerId");
-                String dataJson = data.get("data");
-                String logId = data.get("logId");
-                //todo 业务!!!!!!1.ai沟通总结2.流失风险等级3.沟通摘要4.客户画像8.客户关注点9.客户意向度 //都要异步处理
-                crmCustomerAnalyzeService.aiGeneratedCustomerPortrait(customerId,dataJson,logId);
-
-
-                // 模拟业务处理
-                Thread.sleep(10);
+                for (Map<String, Object> data : batch) {
+                processSingleCustomer(data, successCount, failCount);
             }
 
             long costTime = System.currentTimeMillis() - batchStartTime;
-            successCount.addAndGet(batch.size());
             log.info("线程 {} 批次处理完成, 数据量: {}, 耗时: {}ms",
                     threadName, batch.size(), costTime);
-
         } catch (Exception e) {
             failCount.addAndGet(batch.size());
             log.error("线程 {} 批次处理失败, 数据量: {}", threadName, batch.size(), e);
@@ -123,29 +102,60 @@ public class CrmCustomerAiProcessingTask {
         }
     }
     /**
-     * 处理失败的数据
+     * 处理单个客户的AI分析(6个接口并行)
      */
-    private void handleFailedData(List<List<Map<String, String>>> partitions, int successCount) {
+    private void processSingleCustomer(Map<String, Object> data,
+                                       AtomicInteger successCount,
+                                       AtomicInteger failCount)  {
         try {
-            // 找出未成功处理的数据
-            List<Map<String, String>> failedData = partitions.stream()
-                    .flatMap(List::stream)
-                    .skip(successCount)
-                    .collect(Collectors.toList());
-
-            if (!failedData.isEmpty()) {
-                String failedKey = CRM_AI_REDIS_KEY + ":failed";
-                for (Map<String, String> data : failedData) {
-                    redisTemplate.opsForList().rightPush(failedKey, data);
-                }
-                log.info("失败数据已移至失败队列: {}, 数量: {}", failedKey, failedData.size());
-            }
-
-            // 清理已处理的数据(可选:根据业务需求决定是否删除)
-            // cleanProcessedData(partitions, successCount);
+            Long customerId = (Long)data.get("customerId");
+            String dataJson = (String)data.get("data");
+            Long logId = (Long)data.get("logId");
+
+            long startTime = System.currentTimeMillis();
+
+            // 6 个 AI 接口并行;使用 commonPool,避免与批次线程池 executorService 嵌套导致死锁
+            Executor asyncPool = ForkJoinPool.commonPool();
+            // 使用 supplyAsync 获取返回值,定义具体返回类型
+            CompletableFuture<String> portraitFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiGeneratedCustomerPortrait(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<String> summaryFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCommunicationSummary(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<String> abstractFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCommunicationAbstract(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<Long> attritionFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiAttritionLevel(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<String> focusFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCustomerFocus(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<String> intentionFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiIntentionDegree(customerId, dataJson, logId), asyncPool);
+
+// 等待所有异步任务完成
+            CompletableFuture.allOf(portraitFuture, summaryFuture, abstractFuture,
+                    attritionFuture, focusFuture, intentionFuture).join();
+        //      allAiFutures.get(60, TimeUnit.SECONDS);
+            CrmCustomerAnalyze crmCustomerAnalyze = new CrmCustomerAnalyze();
+            crmCustomerAnalyze.setCustomerId(customerId);
+            crmCustomerAnalyze.setCustomerPortraitJson(portraitFuture.get());
+            crmCustomerAnalyze.setCommunicationSummary(summaryFuture.get());
+            crmCustomerAnalyze.setCommunicationAbstract(abstractFuture.get());
+            crmCustomerAnalyze.setAttritionLevel(attritionFuture.get());
+            crmCustomerAnalyze.setCustomerFocusJson(focusFuture.get());
+            crmCustomerAnalyze.setIntentionDegree(intentionFuture.get());
+            Integer i = crmCustomerAnalyzeService.updateCrmCustomerAnalyzeByCustomerId(crmCustomerAnalyze);
+            long costTime = System.currentTimeMillis() - startTime;
+            successCount.incrementAndGet();
+            log.info("客户 {} 的AI分析完成, 耗时: {}ms,更新{}条", customerId, costTime,i);
 
         } catch (Exception e) {
-            log.error("处理失败数据异常", e);
+            failCount.incrementAndGet();
+            log.error("处理客户数据失败, customerId: {}, logId: {}",
+                    data.get("customerId"), data.get("logId"), e);
         }
     }
 }

+ 1 - 1
fs-ai-call-task/src/main/java/com/fs/app/task/Task.java

@@ -38,7 +38,7 @@ public class Task {
 //    }
 
 
-    @Scheduled(cron = "0 0/1 * * * ?")
+    @Scheduled(cron = "0/30 * * * * ?")
     public void cidWorkflowRun(){
         taskService.cidWorkflowCallRun();
     }

+ 27 - 4
fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -190,11 +190,26 @@ public class FsUserCourseVideoController extends AppBaseController {
 
         R courseSortLink = fsUserCourseService.createCourseSortLink(fsCourseLinkCreateParam);
         String url = courseSortLink.get("url").toString();
+        String linkId=courseSortLink.get("linkId").toString();
         Map<String, Object> map = new HashMap<>();
         map.put("url", url);
+        map.put("linkId", linkId);
         return R.ok(map);
     }
 
+    /**
+     * @Description: 生成看课记录 中康APP 调取接口/courseSortLink 后发送后再生成看课记录
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2026/1/21 15:38
+     */
+    @Login
+    @PostMapping("/batchCreateCourseRecord")
+    public R batchCreateCourseRecord(@RequestBody BatchSendCourseDTO batchSendCourseDTO) {
+        return fsUserCourseService.batchCreateCourseRecord(batchSendCourseDTO);
+    }
+
     @Login
     @PostMapping("/courseImage")
     @ApiOperation("生成课程海报")
@@ -330,7 +345,7 @@ public class FsUserCourseVideoController extends AppBaseController {
         return ResponseResult.ok(liveService.getGotoWxAppLiveLink(linkStr,appid));
     }
 
-    @ApiOperation("会员批量发送课程消息")
+    @ApiOperation("会员批量发送课程消息 发课")
     @PostMapping("/batchSendCourse")
     public OpenImResponseDTO batchSendCourse(@RequestBody BatchSendCourseDTO batchSendCourseDTO) throws JsonProcessingException {
         // 生成看课短链
@@ -339,10 +354,18 @@ public class FsUserCourseVideoController extends AppBaseController {
         R courseSortLink = fsUserCourseService.createAppCourseSortLink(fsCourseLinkCreateParam);
         String url = courseSortLink.get("url").toString();
         batchSendCourseDTO.setUrl(url);
-        batchSendCourseDTO.setIsUrgeCourse(false);
-        return openIMService.batchSendCourse(batchSendCourseDTO);
-    }
+        batchSendCourseDTO.setLinkId(Long.parseLong(courseSortLink.get("linkId").toString()));
+        if(batchSendCourseDTO.getIsUrgeCourse()==null){
+            batchSendCourseDTO.setIsUrgeCourse(false);
+        }
 
+        // 异步调用
+        openIMService.batchSendCourseLimit(batchSendCourseDTO);
+        OpenImResponseDTO openImResponseDTO = new OpenImResponseDTO();
+        openImResponseDTO.setErrCode(0);
+        openImResponseDTO.setErrMsg("异步发送,详细请看明细");
+        return openImResponseDTO;
+    }
     @ApiOperation("会员一键催课")
     @PostMapping("/batchUrgeCourse")
     public OpenImResponseDTO batchUrgeCourse(@RequestBody BatchUrgeCourseDTO batchUrgeCourseDTO) throws JsonProcessingException {

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

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

@@ -12,12 +12,15 @@ import com.fs.aicall.utils.StringUtils;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
@@ -52,6 +55,9 @@ public class CcLlmAgentAccountController extends BaseController
     @Autowired
     private TokenService tokenService;
 
+    @Autowired
+    SysConfigMapper sysConfigMapper;
+
     private static List<String> hideKeys = Arrays.asList("apiKey", "oauthPrivateKey", "oauthPublicKeyId", "patToken");
 
 
@@ -157,6 +163,17 @@ public class CcLlmAgentAccountController extends BaseController
         if (null == ccLlmAgentAccount.getKbCatId()) {
             ccLlmAgentAccount.setKbCatId(-1);
         }
+        if("DeepSeekChat".equalsIgnoreCase(ccLlmAgentAccount.getProviderClassName())){
+            JSONObject jsonObject = JSONObject.parseObject( ccLlmAgentAccount.getAccountJson());
+            if(null != jsonObject && jsonObject.containsKey("apiKey") && jsonObject.getString("apiKey").contains("**")){
+                SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("cId.config");
+                if(null != sysConfig && StringUtils.isNotBlank(sysConfig.getConfigValue())){
+                    JSONObject configValue = JSONObject.parseObject(sysConfig.getConfigValue());
+                    jsonObject.put("apiKey", configValue.getString("apiKey"));
+                    ccLlmAgentAccount.setAccountJson(JSONObject.toJSONString(jsonObject));
+                }
+            }
+        }
         
         // 新增模型
         int result = ccLlmAgentAccountService.insertCcLlmAgentAccount(ccLlmAgentAccount);
@@ -330,4 +347,11 @@ public class CcLlmAgentAccountController extends BaseController
         }
     }
 
+    @GetMapping("/getCidConfig")
+    @ResponseBody
+    public R getCidConfig()
+    {
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("cId.config");
+        return R.ok().put("data",sysConfig.getConfigValue());
+    }
 }

+ 59 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceCloneController.java

@@ -0,0 +1,59 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.company.service.ICompanyVoiceCloneService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 豆包声音克隆 Controller
+ *
+ * @author fs
+ */
+@RestController
+@RequestMapping("/company/voiceClone")
+public class CompanyVoiceCloneController extends BaseController {
+
+    @Autowired
+    private ICompanyVoiceCloneService companyVoiceCloneService;
+
+    /**
+     * 上传音频文件并训练声音克隆音色
+     *
+     * @param file       音频文件(wav/mp3/ogg/m4a/aac)
+     * @param voiceName  音色名称
+     * @param speakerId  声音ID
+     * @param language   语种(0-中文, 1-英文, 2-日语, 3-西班牙语, 4-印尼语, 5-葡萄牙语, 6-德语, 7-法语)
+     * @param modelType  模型类型(1-ICL1.0, 2-DiT标准版, 3-DiT还原版, 4-ICL2.0)
+     */
+    @Log(title = "声音克隆-上传训练", businessType = BusinessType.IMPORT)
+    @PostMapping("/uploadAndTrain")
+    public AjaxResult uploadAndTrain(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam("voice_name") String voiceName,
+            @RequestParam("speaker_id") String speakerId,
+            @RequestParam(value = "language", defaultValue = "0") Integer language,
+            @RequestParam(value = "model_type", defaultValue = "2") Integer modelType) {
+        return companyVoiceCloneService.uploadAndTrain(voiceName, speakerId, language, modelType, file);
+    }
+
+    /**
+     * TTS 语音合成测试
+     *
+     * @param speakerId  声音ID
+     * @param language   语种
+     * @param text       要合成的文本
+     */
+    @Log(title = "声音克隆-TTS测试", businessType = BusinessType.OTHER)
+    @PostMapping("/doubaoTtsTest")
+    public AjaxResult doubaoTtsTest(
+            @RequestParam("speakerId") String speakerId,
+            @RequestParam(value = "language", defaultValue = "0") Integer language,
+            @RequestParam("text") String text) {
+        return companyVoiceCloneService.doubaoTtsTest(speakerId, language, text);
+    }
+}

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

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

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

@@ -76,6 +76,7 @@ public class CompanyWorkflowController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId(loginUser.getUser().getCompanyId());
         param.setCompanyUserId(loginUser.getUser().getUserId());
+        param.setUserName(loginUser.getUsername());
         Long workflowId = companyWorkflowService.saveCompanyWorkflow(param);
         return AjaxResult.success(workflowId);
     }
@@ -178,4 +179,30 @@ public class CompanyWorkflowController extends BaseController {
         return R.ok().put("data",optionVOS);
     }
 
+    /**
+     * 查询某个工作流的版本列表
+     */
+    @GetMapping("/versionList/{workflowId}")
+    public AjaxResult versionList(@PathVariable Long workflowId) {
+        return AjaxResult.success(companyWorkflowService.selectVersionListByWorkflowId(workflowId));
+    }
+
+    /**
+     * 查询某个版本详情
+     */
+    @GetMapping("/versionDetail/{versionId}")
+    public AjaxResult versionDetail(@PathVariable Long versionId) {
+        return AjaxResult.success(companyWorkflowService.selectVersionDetailByVersionId(versionId));
+    }
+
+    /**
+     * 回退到指定版本
+     */
+    @PostMapping("/versionRollback/{versionId}")
+    public AjaxResult rollbackVersion(@PathVariable Long versionId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long workflowId = companyWorkflowService.rollbackWorkflowVersion(versionId,loginUser.getUsername());
+        return AjaxResult.success(workflowId);
+    }
+
 }

+ 96 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java

@@ -4,8 +4,10 @@ import cn.hutool.core.util.ObjectUtil;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.service.impl.CompanyDeptServiceImpl;
@@ -27,6 +29,8 @@ import com.fs.qw.service.IQwWatchLogService;
 import com.fs.qw.vo.QwWatchLogAllStatisticsListVO;
 import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 import com.fs.sop.mapper.SopUserLogsMapper;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -382,4 +386,96 @@ public class FsCourseWatchLogController extends BaseController
     {
         return toAjax(fsCourseWatchLogService.deleteFsCourseWatchLogByLogIds(logIds));
     }
+
+    /**
+     * 查询课程小结详情总体数据
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 总体统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @GetMapping("/courseStatisticsDetail")
+    public R getCourseStatisticsDetail(@RequestParam("videoId") Long videoId, @RequestParam("periodId") Long periodId)
+    {
+        if (videoId == null || periodId == null) {
+            return R.error("视频ID和营期ID不能为空");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null || loginUser.getCompany() == null || loginUser.getCompany().getCompanyId() == null){
+            return R.error("销售信息不存在!");
+        }
+        FsCourseSummaryDetailQueryParam param = new FsCourseSummaryDetailQueryParam();
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if(!"00".equals(loginUser.getUser().getUserType())){
+            param.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        return R.ok().put("data", fsCourseWatchLogService.getCourseStatisticsDetail(param));
+    }
+
+    /**
+     * 课程小结-用户详情列表(分页)
+     * 根据videoId、periodId查询观看记录,区分首次/第2-n次观看时长,关联订单及销售公司
+     *
+     * @param videoId  视频ID
+     * @param periodId 营期ID
+     * @param pageNum  页码
+     * @param pageSize 每页条数
+     * @return 分页用户详情
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @GetMapping("/courseStatisticsUserDetail")
+    public R getCourseStatisticsUserDetail(
+            @RequestParam("videoId") Long videoId,
+            @RequestParam("periodId") Long periodId,
+            @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+            @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
+        if (videoId == null || periodId == null) {
+            return R.error("视频ID和营期ID不能为空");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null || loginUser.getCompany() == null || loginUser.getCompany().getCompanyId() == null){
+            return R.error("销售信息不存在!");
+        }
+        com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        param.setPageNum(pageNum);
+        param.setPageSize(pageSize);
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if(!"00".equals(loginUser.getUser().getUserType())){
+            param.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+        PageHelper.startPage(pageNum, pageSize);
+        return R.ok().put("data", new PageInfo<>(fsCourseWatchLogService.getCourseStatisticsUserDetailList(param)));
+    }
+
+    /**
+     * 课程小结-用户详情导出(按创建时间倒序,最多50000条)
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @Log(title = "课程小结用户详情导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/courseStatisticsUserDetailExport")
+    public AjaxResult courseStatisticsUserDetailExport(
+            @RequestParam("videoId") Long videoId,
+            @RequestParam("periodId") Long periodId) {
+        if (videoId == null || periodId == null) {
+            return AjaxResult.error("视频ID和营期ID不能为空");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null || loginUser.getCompany() == null || loginUser.getCompany().getCompanyId() == null){
+            throw new ServiceException("销售信息不存在!");
+        }
+        com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if(!"00".equals(loginUser.getUser().getUserType())){
+            param.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+        List<com.fs.course.vo.CourseStatisticsUserDetailVO> list = fsCourseWatchLogService.getCourseStatisticsUserDetailExportList(param);
+        ExcelUtil<com.fs.course.vo.CourseStatisticsUserDetailVO> util = new ExcelUtil<>(com.fs.course.vo.CourseStatisticsUserDetailVO.class);
+        return util.exportExcel(list, "用户看课数据");
+    }
 }

+ 28 - 2
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerAnalyzeController.java

@@ -1,6 +1,9 @@
 package com.fs.company.controller.crm;
 
 import java.util.List;
+
+import com.fs.common.core.domain.R;
+import com.fs.crm.param.PolishingScriptParam;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -22,7 +25,7 @@ import com.fs.common.core.page.TableDataInfo;
 
 /**
  * 客户聊天记录分析Controller
- * 
+ *
  * @author fs
  * @date 2026-03-24
  */
@@ -45,6 +48,18 @@ public class CrmCustomerAnalyzeController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询所有客户(根据客户分组取客户最新一条数据)
+     */
+    @GetMapping("/listAll")
+    public TableDataInfo listAll(CrmCustomerAnalyze crmCustomerAnalyze)
+    {
+        startPage();
+        List<CrmCustomerAnalyze> list = crmCustomerAnalyzeService.selectCrmCustomerAnalyzeListAll(crmCustomerAnalyze);
+        return getDataTable(list);
+    }
+
+
     /**
      * 导出客户聊天记录分析列表
      */
@@ -54,7 +69,7 @@ public class CrmCustomerAnalyzeController extends BaseController
     public AjaxResult export(CrmCustomerAnalyze crmCustomerAnalyze)
     {
         List<CrmCustomerAnalyze> list = crmCustomerAnalyzeService.selectCrmCustomerAnalyzeList(crmCustomerAnalyze);
-        ExcelUtil<CrmCustomerAnalyze> util = new ExcelUtil<CrmCustomerAnalyze>(CrmCustomerAnalyze.class);
+        ExcelUtil<CrmCustomerAnalyze> util = new ExcelUtil<>(CrmCustomerAnalyze.class);
         return util.exportExcel(list, "客户聊天记录分析数据");
     }
 
@@ -100,4 +115,15 @@ public class CrmCustomerAnalyzeController extends BaseController
     {
         return toAjax(crmCustomerAnalyzeService.deleteCrmCustomerAnalyzeByIds(ids));
     }
+
+    /**
+     * 话术润色
+     */
+    @PreAuthorize("@ss.hasPermi('crm:analyze:polishingScript')")
+    @PostMapping("/polishingScript")
+    public R polishingScript(@RequestBody PolishingScriptParam param)
+    {
+        return R.ok().put("data",crmCustomerAnalyzeService.polishingScript(param));
+    }
+
 }

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

@@ -190,13 +190,6 @@ public class CrmCustomerController extends BaseController
 //        if(loginUser.getCompany().getCompanyId()==116){   // 河北湘银信息咨询服务有限公司(JZ-1)客户假删除不显示
 //            param.setCompanyId(0L);
 //        }
-        //默认值处理
-        if(param.getIntentionDegreeGt() != null && param.getIntentionDegreeGt() == 0){
-            param.setIntentionDegreeGt(null);
-        }
-        if(param.getIntentionDegreelt() != null && param.getIntentionDegreelt() == 0){
-            param.setIntentionDegreelt(null);
-        }
         if (param.getIsReceive() != null && param.getIsReceive() == 0){
             CrmLineCustomerListQueryParam param1 = new CrmLineCustomerListQueryParam();
             BeanUtils.copyProperties(param,param1);

+ 107 - 0
fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatMessageController.java

@@ -0,0 +1,107 @@
+package com.fs.company.controller.crm.chat;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.crm.domain.CrmCustomerChatMessage;
+import com.fs.crm.service.ICrmCustomerChatMessageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 聊天消息记录 Controller
+ * 
+ * @author ylrz
+ * @date 2026-03-30
+ */
+@RestController
+@RequestMapping("/crm/customer/chat/chatMsg")
+public class CrmCustomerChatMessageController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerChatMessageService chatMessageService;
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    /**
+     * 查询聊天消息记录列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerChatMessage chatMessage) {
+        // 支持游标分页参数
+        Long cursor = chatMessage.getCursor();
+        Integer limit = chatMessage.getLimit();
+        String direction = chatMessage.getDirection();
+
+        List<CrmCustomerChatMessage> list = chatMessageService.selectChatMessagesBySession(
+            chatMessage.getSessionId(), cursor, limit, direction);
+        return getDataTable(list);
+    }
+
+    /**
+     * 批量保存消息
+     */
+    @PostMapping("/batch")
+    public AjaxResult batchSave(@RequestBody Map<String, Object> params) {
+        try {
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> messagesData = (List<Map<String, Object>>) params.get("messages");
+            
+            if (messagesData == null || messagesData.isEmpty()) {
+                return AjaxResult.error("消息列表不能为空");
+            }
+            
+            // 转换为 ChatMessage 对象
+            List<CrmCustomerChatMessage> messages = convertToChatMessages(messagesData);
+            
+            int result = chatMessageService.batchSaveChatMessages(messages);
+            return result > 0 ? AjaxResult.success("保存成功") : AjaxResult.error("保存失败");
+        } catch (Exception e) {
+            logger.error("批量保存消息失败", e);
+            return AjaxResult.error("批量保存失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 新增聊天消息记录
+     */
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmCustomerChatMessage chatMessage) {
+        return toAjax(chatMessageService.insertChatMessage(chatMessage));
+    }
+
+    /**
+     * 修改聊天消息记录
+     */
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerChatMessage chatMessage) {
+        return toAjax(chatMessageService.updateChatMessage(chatMessage));
+    }
+
+    /**
+     * 删除聊天消息记录
+     */
+    @DeleteMapping("/{msgIds}")
+    public AjaxResult remove(@PathVariable Long[] msgIds) {
+        return toAjax(chatMessageService.deleteChatMessageByIds(msgIds));
+    }
+
+    /**
+     * 将 Map 转换为 ChatMessage 对象
+     */
+    private List<CrmCustomerChatMessage> convertToChatMessages(List<Map<String, Object>> messagesData) {
+        try {
+            // 使用 Jackson ObjectMapper 进行类型转换
+            String json = objectMapper.writeValueAsString(messagesData);
+            return objectMapper.readValue(json, objectMapper.getTypeFactory()
+                    .constructCollectionType(List.class, CrmCustomerChatMessage.class));
+        } catch (Exception e) {
+            logger.error("转换消息数据失败", e);
+            throw new RuntimeException("消息数据转换失败:" + e.getMessage());
+        }
+    }
+}

+ 156 - 0
fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatSessionController.java

@@ -0,0 +1,156 @@
+package com.fs.company.controller.crm.chat;
+
+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.utils.ServletUtils;
+import com.fs.crm.domain.CrmCustomerChatMessage;
+import com.fs.crm.domain.CrmCustomerChatSession;
+import com.fs.crm.service.ICrmCustomerChatMessageService;
+import com.fs.crm.service.ICrmCustomerChatSessionService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 聊天会话 Controller
+ * 
+ * @author ylrz
+ * @date 2026-03-30
+ */
+@RestController
+@RequestMapping("/crm/customer/chat/chatSession")
+public class CrmCustomerChatSessionController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerChatSessionService chatSessionService;
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private ICrmCustomerChatMessageService chatMessageService;
+
+    /**
+     * 查询聊天会话列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerChatSession chatSession) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        chatSession.setUserId(loginUser.getUser().getUserId());
+        List<CrmCustomerChatSession> list = chatSessionService.selectChatSessionList(chatSession);
+        //根据会话查询对应的消息数据
+        list.forEach(session -> {
+            List<CrmCustomerChatMessage> messageList =chatMessageService.selectChatMessageBySessionIdLimit(session.getSessionId());
+            session.setMessageList(messageList);
+        });
+
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取会话详情
+     */
+    @GetMapping("/{sessionId}")
+    public AjaxResult getInfo(@PathVariable Long sessionId) {
+        return AjaxResult.success(chatSessionService.selectChatSessionById(sessionId));
+    }
+
+    /**
+     * 新增聊天会话
+     */
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmCustomerChatSession chatSession) {
+        // 设置默认值
+        if (chatSession.getTitle() == null || chatSession.getTitle().isEmpty()) {
+            chatSession.setTitle("新会话");
+        }
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        chatSession.setUserId(loginUser.getUser().getUserId());
+        int result = chatSessionService.insertChatSession(chatSession);
+        if (result > 0) {
+            return AjaxResult.success(chatSession);
+        }
+        return error("新增失败");
+    }
+
+    /**
+     * 更新会话标题
+     */
+    @PutMapping("/title")
+    public AjaxResult updateTitle(@RequestBody Map<String, Object> params) {
+        try {
+            Long sessionId = Long.valueOf(params.get("sessionId").toString());
+            String title = (String) params.get("title");
+            
+            if (title == null) {
+                return error("参数错误");
+            }
+            
+            int result = chatSessionService.updateChatSessionTitle(sessionId, title);
+            return result > 0 ? success("更新成功") : error("更新失败");
+        } catch (Exception e) {
+            logger.error("更新会话标题失败", e);
+            return error("更新失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 更新置顶状态
+     */
+    @PutMapping("/pin")
+    public AjaxResult updatePin(@RequestBody Map<String, Object> params) {
+        try {
+            Long sessionId = Long.valueOf(params.get("sessionId").toString());
+            Integer isPinned = (Integer) params.get("isPinned");
+            
+            if (isPinned == null) {
+                return error("置顶参数错误");
+            }
+            int result = chatSessionService.updateChatSessionPin(sessionId, isPinned);
+            return result > 0 ? success("置顶成功") : error("置顶失败");
+        } catch (Exception e) {
+            return error("置顶失败:" + e.getMessage());
+        }
+    }
+    /**
+     * 更新客户绑定
+     */
+    @PutMapping("/customer")
+    public AjaxResult updateCustomer(@RequestBody Map<String, Object> params) {
+        try {
+            Long sessionId = Long.valueOf(params.get("sessionId").toString());
+            Long customerId = Long.parseLong(params.get("customerId").toString());
+            int result = chatSessionService.updateChatSessionCustomer(sessionId, customerId);
+            return result > 0 ? success("更新客户绑定成功") : error("更新客户绑定失败");
+        } catch (Exception e) {
+            return error("更新客户绑定失败:" + e.getMessage());
+        }
+    }
+    /**
+     * 修改聊天会话
+     */
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerChatSession chatSession) {
+        return toAjax(chatSessionService.updateChatSession(chatSession));
+    }
+
+    /**
+     * 删除聊天会话
+     */
+    @DeleteMapping("/{sessionId}")
+    public AjaxResult remove(@PathVariable Long sessionId) {
+        return toAjax(chatSessionService.deleteChatSessionById(sessionId));
+    }
+
+    /**
+     * 批量删除聊天会话
+     */
+    @DeleteMapping
+    public AjaxResult removeBatch(@RequestBody Long[] sessionIds) {
+        return toAjax(chatSessionService.deleteChatSessionByIds(sessionIds));
+    }
+}

+ 65 - 0
fs-company/src/main/java/com/fs/company/controller/im/FsImMsgSendLogController.java

@@ -0,0 +1,65 @@
+package com.fs.company.controller.im;
+
+import com.fs.app.service.param.FsImMsgSendLogRequest;
+import com.fs.app.service.param.FsImMsgSendLogResponse;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.im.service.IFsImMsgSendLogService;
+import com.github.pagehelper.PageHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * OpenIM 消息发送记录主表 Controller
+ */
+@RestController
+@RequestMapping("/app/im")
+public class FsImMsgSendLogController extends BaseController {
+
+    @Autowired
+    private IFsImMsgSendLogService fsImMsgSendLogService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询 OpenIM 消息发送记录列表
+     */
+    @GetMapping("/listImMsgSendLog")
+    public TableDataInfo list(FsImMsgSendLogRequest request) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        request.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        PageHelper.startPage(request.getPageNum(), request.getPageSize());
+        List<FsImMsgSendLogResponse> list = fsImMsgSendLogService.selectFsImMsgSendLogInfoList(request);
+        return getDataTable(list);
+    }
+
+//    /**
+//     * 导出 OpenIM 消息发送记录列表
+//     */
+//    @GetMapping("/exportImMsgSendLog")
+//    public void export(HttpServletResponse response, FsImMsgSendLogRequest request) {
+//        List<FsImMsgSendLog> list = fsImMsgSendLogService.exportFsImMsgSendLog(request);
+//        ExcelUtil<FsImMsgSendLog> util = new ExcelUtil<>(FsImMsgSendLog.class);
+//        util.exportExcel(response, list, "OpenIM 消息发送记录");
+//    }
+//
+    /**
+     * 获取发送状态统计
+     */
+    @GetMapping("/getImMsgSendStatistics")
+    public R statistics(FsImMsgSendLogRequest request) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        request.setCompanyId(loginUser.getCompany().getCompanyId());
+        return fsImMsgSendLogService.getFsImMsgSendStatistics(request);
+    }
+}

+ 10 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -5,6 +5,7 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.DateUtils;
@@ -12,7 +13,6 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.http.HttpUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
-import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
 import com.fs.his.domain.FsPayConfig;
@@ -402,4 +402,13 @@ public class LiveController extends BaseController
 
         return R.ok().put("data", exist);
     }
+
+    @ApiOperation("创建App跳转通用链接")
+    @GetMapping("/createAppLink")
+    @PreAuthorize("@ss.hasPermi('live:live:createAppLink')")
+    public R createAppLink(@RequestParam("liveId") Long liveId,@RequestParam("corpId")String corpId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        return liveService.createAppLink(user,liveId,corpId);
+    }
+
 }

+ 192 - 0
fs-company/src/main/java/com/fs/company/controller/statistic/courseStatisticController.java

@@ -0,0 +1,192 @@
+package com.fs.company.controller.statistic;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.his.vo.AppSalesCourseStatisticsVO;
+import com.fs.his.vo.AppSalesWatchLogReportVO;
+import com.fs.his.vo.AppWatchLogReportVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @description: app 看课统计
+ * @author: Xgb
+ * @createDate: 2026/3/16
+ * @version: 1.0
+ */
+@RestController
+@RequestMapping("/app/statistics")
+public class courseStatisticController extends BaseController {
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private IFsCourseWatchLogService courseWatchLogService;
+
+    /**
+     * 销售后台app看课统计 会员维度
+     * @param param
+     * @return
+     */
+    @GetMapping("/appWatchLogReport")
+    public TableDataInfo appWatchLogReport(FsCourseWatchLogStatisticsListParam param) {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        return getDataTable(courseWatchLogService.selectUserAppWatchLogReportVO(param));
+    }
+
+    /**
+     * 销售后台app看课统计 会员维度导出
+     * @param param
+     * @return
+     */
+    @GetMapping("/appWatchLogReportExport")
+    public AjaxResult appWatchLogReportExport(FsCourseWatchLogStatisticsListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AppWatchLogReportVO> list = courseWatchLogService.selectUserAppWatchLogReportVO(param);
+        // 转换登录渠道和答题状态
+        list.forEach(this::convertAppWatchLogReportVO);
+        // 获取所有字段,排除会员维度的三个字段
+        List<String> selectedFields = getAppWatchLogReportFields();
+        ExcelUtil<AppWatchLogReportVO> util = new ExcelUtil<AppWatchLogReportVO>(AppWatchLogReportVO.class);
+        return util.exportExcelSelectedColumns(list, "APP看课统计报表", selectedFields);
+    }
+
+    /**
+     * 转换AppWatchLogReportVO字段
+     * - 登录渠道:有值显示"app",无值显示"小程序"
+     * - 答题状态:无值显示"未答题"
+     */
+    private void convertAppWatchLogReportVO(AppWatchLogReportVO vo) {
+        // 登录渠道转换
+        if (StringUtils.isNotEmpty(vo.getLoginChannel())) {
+            vo.setLoginChannel("app");
+        } else {
+            vo.setLoginChannel("小程序");
+        }
+        // 答题状态转换
+        if (StringUtils.isEmpty(vo.getAnswerStatus())) {
+            vo.setAnswerStatus("未答题");
+        }
+    }
+
+    /**
+     * 获取AppWatchLogReportVO需要导出的字段(排除特定字段)
+     */
+    private List<String> getAppWatchLogReportFields() {
+        // 需要排除的字段:app会员数、销售数、新注册app会员数
+        List<String> excludeFields = Arrays.asList("AppUserCount", "salesCount", "AppNewUser");
+
+        return Arrays.stream(AppWatchLogReportVO.class.getDeclaredFields())
+                .filter(field -> field.isAnnotationPresent(Excel.class))
+                .map(Field::getName)
+                .filter(fieldName -> !excludeFields.contains(fieldName))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * APP端销售维度看课统计报表
+     * 注意:必须放在 /{logId} 之前,避免路径冲突
+     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:appSalesReport')")
+    @GetMapping("/appSalesWatchLogReport")
+    public TableDataInfo appSalesWatchLogReport(FsCourseWatchLogStatisticsListParam param)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AppSalesWatchLogReportVO> list = courseWatchLogService.selectAppSalesWatchLogReportVO(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出APP端销售维度看课统计报表
+     */
+    @Log(title = "APP销售维度看课统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/appSalesWatchLogReportExport")
+    public AjaxResult appSalesWatchLogReportExport(FsCourseWatchLogStatisticsListParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AppSalesWatchLogReportVO> list = courseWatchLogService.selectAppSalesWatchLogReportVO(param);
+        // 根据维度获取需要导出的字段
+        List<String> selectedFields = getAppSalesWatchLogReportFields(param.getDimension());
+        ExcelUtil<AppSalesWatchLogReportVO> util = new ExcelUtil<AppSalesWatchLogReportVO>(AppSalesWatchLogReportVO.class);
+        return util.exportExcelSelectedColumns(list, "APP销售维度看课统计报表", selectedFields);
+    }
+
+
+    /**
+     * 获取AppSalesWatchLogReportVO需要导出的字段
+     * @param dimension 维度:sales-销售维度, dept-销售部门维度
+     */
+    private List<String> getAppSalesWatchLogReportFields(String dimension) {
+        // 销售维度字段
+        List<String> salesFields = Arrays.asList(
+                "salesName", "appUserCount", "newAppUserCount", "salesDept",
+                "salesCompany", "trainingCampName", "periodName", "videoTitle",
+                "finishedCount", "unfinishedCount", "completionRate",
+                "notWatchedCount", "notAnsweredCount", "redPacketAmount", "historyOrderCount"
+        );
+
+        // 销售部门维度字段(去掉销售列,销售数放在销售部门后面)
+        List<String> deptFields = Arrays.asList(
+                "salesDept", "salesCount", "appUserCount", "newAppUserCount",
+                "salesCompany", "trainingCampName", "periodName", "videoTitle",
+                "finishedCount", "unfinishedCount", "completionRate",
+                "notWatchedCount", "notAnsweredCount", "redPacketAmount", "historyOrderCount"
+        );
+
+        return "dept".equals(dimension) ? deptFields : salesFields;
+    }
+
+
+    /**
+     * APP 端看课统计(销售维度)
+     * 对应前端页面:appWatchCourseStatistics.vue
+     */
+    @GetMapping("/appWatchCourseStatistics")
+    public TableDataInfo appWatchCourseStatistics(FsCourseWatchLogStatisticsListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AppSalesCourseStatisticsVO> list = courseWatchLogService.selectAppSalesCourseStatisticsVO(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * APP 端看课统计导出(销售维度)
+     */
+    @Log(title = "APP 看课统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/appWatchCourseStatisticsExport")
+    public AjaxResult appWatchCourseStatisticsExport(FsCourseWatchLogStatisticsListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AppSalesCourseStatisticsVO> list = courseWatchLogService.selectAppSalesCourseStatisticsVO(param);
+
+        ExcelUtil<AppSalesCourseStatisticsVO> util = new ExcelUtil<AppSalesCourseStatisticsVO>(AppSalesCourseStatisticsVO.class);
+        return util.exportExcel(list, "APP 看课统计报表");
+    }
+
+
+}

+ 158 - 39
fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java

@@ -11,6 +11,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ParseUtils;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.crm.service.ICrmCustomerService;
@@ -19,6 +20,7 @@ import com.fs.erp.dto.ErpOrderQueryResponse;
 import com.fs.erp.service.IErpOrderService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
+import com.fs.framework.service.TokenService;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
 import com.fs.his.dto.ExpressInfoDTO;
@@ -44,6 +46,7 @@ import com.fs.hisStore.dto.StoreOrderProductDTO;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
@@ -82,6 +85,12 @@ public class FsStoreOrderController extends BaseController
     @Autowired
     private IFsStoreOrderItemScrmService orderItemScrmService;
 
+    @Value("${cloud_host.company_name}")
+    private String signProjectName;
+
+    @Autowired
+    private TokenService tokenService;
+
     /**
      * 查询直播/点播订单列表(fs_store_order_scrm 中 order_type=2 直播订单,order_type=3 点播订单)
      * 如果前端传了 orderType,则按指定类型查询;如果没传(null),则查询所有直播和点播订单(orderType IN (2,3))
@@ -89,7 +98,7 @@ public class FsStoreOrderController extends BaseController
      */
     @PostMapping("/healthLiveList")
     public FsStoreOrderListAndStatisticsVo healthLiveList(@RequestBody com.fs.hisStore.param.FsStoreOrderParam param) {
-        LoginUser loginUser = SecurityUtils.getLoginUser();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId(loginUser.getCompany().getCompanyId());
         // 如果前端传了 orderType,使用前端传的值;如果没传(null),设置 orderType = -1(特殊值,SQL 中会转换为查询 orderType IN (2,3))
         if (param.getOrderType() == null) {
@@ -115,6 +124,7 @@ public class FsStoreOrderController extends BaseController
         Map<String, java.math.BigDecimal> statistics = fsStoreOrderScrmService.selectFsStoreOrderStatistics(param);
         String productInfoStr = fsStoreOrderScrmService.selectFsStoreOrderProductStatistics(param);
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setIsCompanyOrder(null);
         List<com.fs.hisStore.vo.FsStoreOrderVO> list = fsStoreOrderScrmService.selectFsStoreOrderListVO(param);
         if (list != null) {
             for (com.fs.hisStore.vo.FsStoreOrderVO vo : list) {
@@ -301,25 +311,95 @@ public class FsStoreOrderController extends BaseController
         if (fsStoreOrderScrmService.isEntityNull(param)) {
             return AjaxResult.error("请筛选数据导出");
         }
+        if(!StringUtils.isEmpty(param.getPayTimeRange())){
+            param.setPayTimeList(param.getPayTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getDeliverySendTimeRange())){
+            param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
+            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if("北京卓美".equals(signProjectName)){
+            if(!"00".equals(loginUser.getUser().getUserType())){//非管理员看见自己数据
+                param.setCompanyUserId(loginUser.getUser().getUserId());
+            }
+        }
         List<FsStoreOrderItemExportVO> list = orderItemScrmService.selectFsStoreOrderItemListExportVO(param);
+        if ("北京卓美".equals(signProjectName)) {
+            List<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> zmvoList = list.stream()
+                    .map(vo -> {
+                        com.fs.hisStore.vo.FsStoreOrderItemExportZMVO zmvo = new com.fs.hisStore.vo.FsStoreOrderItemExportZMVO();
+                        try {
+                            BeanUtil.copyProperties(vo, zmvo);
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                        return zmvo;
+                    })
+                    .collect(java.util.stream.Collectors.toList());
+            if (zmvoList != null) {
+                for (com.fs.hisStore.vo.FsStoreOrderItemExportZMVO vo : zmvoList) {
+                    if("2".equals(vo.getOrderType())){
+                        vo.setOrderTypeStr("直播订单" );
+                    }else if ("3".equals(vo.getOrderType())){
+                        vo.setOrderTypeStr("点播订单" );
+                    }else{
+                        vo.setOrderTypeStr("商城订单" );
+                    }
+                    if(StringUtils.isNotEmpty(vo.getJsonInfo())){
+                        try {
+                            StoreOrderProductDTO orderProductDTO = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                            BeanUtil.copyProperties(orderProductDTO, vo);
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                    }
+
+                    if (vo.getUserPhone() != null) {
+                        String phone = vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{1})", "$1****$2");
+                        vo.setUserPhone(phone);
+                    }
+                    if (!StringUtils.isEmpty(vo.getJsonInfo())) {
+                        try {
+                            StoreOrderProductDTO orderProductDTO = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                            BeanUtil.copyProperties(orderProductDTO, vo);
+                        } catch (Exception e) {
+                        }
+                    }
+                    if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) && !java.util.Objects.isNull(vo.getCost())) {
+                        vo.setFPrice(vo.getCost().multiply(java.math.BigDecimal.valueOf(vo.getNum())));
+                    } else {
+                        vo.setPayPostage(java.math.BigDecimal.ZERO);
+                        vo.setPayDelivery(java.math.BigDecimal.ZERO);
+                        vo.setCost(java.math.BigDecimal.ZERO);
+                        vo.setFPrice(java.math.BigDecimal.ZERO);
+                        vo.setBarCode("");
+                        vo.setCateName("");
+                        vo.setBankTransactionId("");
+                    }
+                }
+            }
+            ExcelUtil<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> util = new ExcelUtil<>(com.fs.hisStore.vo.FsStoreOrderItemExportZMVO.class);
+            return util.exportExcel(zmvoList, "订单明细数据");
+        }
+        //对手机号脱敏
         if (list != null) {
-            LoginUser loginUser = SecurityUtils.getLoginUser();
             for (FsStoreOrderItemExportVO vo : list) {
                 if (vo.getUserPhone() != null) {
-                    vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{1})", "$1****$2"));
+                    String phone = vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{1})", "$1****$2");
+                    vo.setUserPhone(phone);
                 }
-                if (StringUtils.isNotEmpty(vo.getJsonInfo())) {
+                if (!StringUtils.isEmpty(vo.getJsonInfo())) {
                     try {
-                        StoreOrderProductDTO dto = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
-                        BeanUtil.copyProperties(dto, vo);
+                        StoreOrderProductDTO orderProductDTO = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                        BeanUtil.copyProperties(orderProductDTO, vo);
                     } catch (Exception e) {
-                        // ignore
                     }
                 }
-                if (vo.getUserAddress() != null) {
-                    vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
-                }
-                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) && vo.getCost() != null) {
+                //
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && !java.util.Objects.isNull(vo.getCost())) {
                     vo.setFPrice(vo.getCost().multiply(java.math.BigDecimal.valueOf(vo.getTotalNum())));
                 } else {
                     vo.setPayPostage(java.math.BigDecimal.ZERO);
@@ -329,17 +409,6 @@ public class FsStoreOrderController extends BaseController
                     vo.setCateName("");
                     vo.setBankTransactionId("");
                 }
-                // 设置订单类型中文显示
-                if (vo.getOrderType() != null) {
-                    String orderTypeStr = vo.getOrderType().toString();
-                    if ("2".equals(orderTypeStr)) {
-                        vo.setOrderType("直播订单");
-                    } else if ("3".equals(orderTypeStr)) {
-                        vo.setOrderType("点播订单");
-                    } else {
-                        vo.setOrderType("商城订单");
-                    }
-                }
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);
@@ -357,19 +426,80 @@ public class FsStoreOrderController extends BaseController
         if (fsStoreOrderScrmService.isEntityNull(param)) {
             return AjaxResult.error("请筛选数据导出");
         }
+        if(!StringUtils.isEmpty(param.getPayTimeRange())){
+            param.setPayTimeList(param.getPayTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getDeliverySendTimeRange())){
+            param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
+            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if("北京卓美".equals(signProjectName)){
+            if(!"00".equals(loginUser.getUser().getUserType())){//非管理员看见自己数据
+                param.setCompanyUserId(loginUser.getUser().getUserId());
+            }
+        }
         List<FsStoreOrderItemExportVO> list = orderItemScrmService.selectFsStoreOrderItemListExportVO(param);
+        // 北京卓美项目特殊处理
+        if("北京卓美".equals(signProjectName)){
+            List<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> zmvoList = list.stream()
+                    .map(vo -> {
+                        com.fs.hisStore.vo.FsStoreOrderItemExportZMVO zmvo = new com.fs.hisStore.vo.FsStoreOrderItemExportZMVO();
+                        try {
+                            BeanUtil.copyProperties( vo,zmvo);
+                        } catch (Exception e) {
+                            // 处理异常
+                            e.printStackTrace();
+                        }
+                        return zmvo;
+                    })
+                    .collect(java.util.stream.Collectors.toList());
+            //对手机号脱敏
+            if (zmvoList != null) {
+                    for (com.fs.hisStore.vo.FsStoreOrderItemExportZMVO vo : zmvoList) {
+                        if ("2".equals(vo.getOrderType())) {
+                            vo.setOrderTypeStr("直播订单");
+                        }else if ("3".equals(vo.getOrderType())){
+                            vo.setOrderTypeStr("点播订单" );
+                        }else {
+                            vo.setOrderTypeStr("商城订单");
+                        }
+                        if (!StringUtils.isEmpty(vo.getJsonInfo())) {
+                            try {
+                                StoreOrderProductDTO orderProductDTO = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                                BeanUtil.copyProperties(orderProductDTO, vo);
+                            } catch (Exception e) {
+                            }
+                        }
+                        if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && vo.getCost() != null) {
+                            vo.setFPrice(vo.getCost().multiply(java.math.BigDecimal.valueOf(vo.getNum())));
+                        } else {
+                            vo.setPayPostage(java.math.BigDecimal.ZERO);
+                            vo.setPayDelivery(java.math.BigDecimal.ZERO);
+                            vo.setCost(java.math.BigDecimal.ZERO);
+                            vo.setFPrice(java.math.BigDecimal.ZERO);
+                            vo.setBarCode("");
+                            vo.setCateName("");
+                            vo.setBankTransactionId("");
+                        }
+                    }
+                }
+                ExcelUtil<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> util = new ExcelUtil<>(com.fs.hisStore.vo.FsStoreOrderItemExportZMVO.class);
+                return util.exportExcel(zmvoList, "订单明细数据");
+        }
+        //对手机号脱敏
         if (list != null) {
-            LoginUser loginUser = SecurityUtils.getLoginUser();
             for (FsStoreOrderItemExportVO vo : list) {
-                if (StringUtils.isNotEmpty(vo.getJsonInfo())) {
+                if (!StringUtils.isEmpty(vo.getJsonInfo())) {
                     try {
-                        StoreOrderProductDTO dto = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
-                        BeanUtil.copyProperties(dto, vo);
+                        StoreOrderProductDTO orderProductDTO = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                        BeanUtil.copyProperties(orderProductDTO, vo);
                     } catch (Exception e) {
-                        // ignore
                     }
                 }
-                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) && vo.getCost() != null) {
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && vo.getCost() != null) {
                     vo.setFPrice(vo.getCost().multiply(java.math.BigDecimal.valueOf(vo.getTotalNum())));
                 } else {
                     vo.setPayPostage(java.math.BigDecimal.ZERO);
@@ -379,17 +509,6 @@ public class FsStoreOrderController extends BaseController
                     vo.setCateName("");
                     vo.setBankTransactionId("");
                 }
-                // 设置订单类型中文显示
-                if (vo.getOrderType() != null) {
-                    String orderTypeStr = vo.getOrderType().toString();
-                    if ("2".equals(orderTypeStr)) {
-                        vo.setOrderType("直播订单");
-                    } else if ("3".equals(orderTypeStr)) {
-                        vo.setOrderType("点播订单");
-                    } else {
-                        vo.setOrderType("商城订单");
-                    }
-                }
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);

+ 24 - 2
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -129,6 +129,15 @@ public class FsStoreOrderScrmController extends BaseController
             }
         }
         startPage();
+
+        if("北京卓美".equals(cloudHostProper.getCompanyName())){
+            if(!"00".equals(loginUser.getUser().getUserType())){//非管理员看见自己数据
+                param.setCompanyUserId(loginUser.getUser().getUserId());
+            }
+            param.setIsCompanyOrder(1);//是否销售订单
+        }
+
+
         if(!StringUtils.isEmpty(param.getCreateTimeRange())){
             param.setCreateTimeList(param.getCreateTimeRange().split("--"));
         }
@@ -206,6 +215,12 @@ public class FsStoreOrderScrmController extends BaseController
         if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
             param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
         }
+        if("北京卓美".equals(cloudHostProper.getCompanyName())){
+            if(!"00".equals(loginUser.getUser().getUserType())){//非管理员看见自己数据
+                param.setCompanyUserId(loginUser.getUser().getUserId());
+            }
+            param.setIsCompanyOrder(1);//是否销售订单
+        }
         List<FsStoreOrderErpExportVO> list = fsStoreOrderService.selectFsStoreOrderListVOByExport(param);
         //对手机号脱敏
         if(list!=null){
@@ -279,7 +294,9 @@ public class FsStoreOrderScrmController extends BaseController
         }
 
         FsUser user=userService.selectFsUserById(order.getUserId());
-        user.setPhone(ParseUtils.parsePhone(user.getPhone()));
+        if(user != null && StringUtils.isNotEmpty(user.getPhone())){
+            user.setPhone(ParseUtils.parsePhone(user.getPhone()));
+        }
         FsStoreOrderItemScrm itemMap=new FsStoreOrderItemScrm();
         itemMap.setOrderId(order.getId());
         List<FsStoreOrderItemScrm> items=orderItemService.selectFsStoreOrderItemList(itemMap);
@@ -502,7 +519,12 @@ public class FsStoreOrderScrmController extends BaseController
         if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
             param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
         }
-
+        if("北京卓美".equals(cloudHostProper.getCompanyName())){
+            if(!"00".equals(loginUser.getUser().getUserType())){//非管理员看见自己数据
+                param.setCompanyUserId(loginUser.getUser().getUserId());
+            }
+            param.setIsCompanyOrder(1);//是否销售订单
+        }
         List<FsStoreOrderItemExportVO> list=orderItemService.selectFsStoreOrderItemListExportVO(param);
         //对手机号脱敏
         if(list!=null){

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -774,7 +774,7 @@ public class IpadSendServer {
         }
         sendShortLink = sendShortLink.replace(".html","");
         String InvitationCode = LinkUtil.encryptLink(sendShortLink);
-        TxtVo txtVo = TxtVo.builder().content(InvitationCode).build();
+        TxtVo txtVo = TxtVo.builder().content("康好健康"+InvitationCode).build();
         txtVo.setBase(vo);
         WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> resp = ipadSendUtils.sendTxt(txtVo);
         if (resp.getErrcode() != 0) {

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

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

@@ -0,0 +1,31 @@
+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;
+
+    private Long roboticId;
+
+    private String workflowInstanceId;
+
+    private Long companyId;
+
+    private Long companyUserId;
+
+
+}

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

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

@@ -0,0 +1,520 @@
+package com.fs.aiSipCall.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.common.core.redis.RedisCache;
+import com.fs.company.domain.CompanyAiWorkflowExec;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.mapper.CompanyAiWorkflowExecMapper;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
+import com.fs.company.mapper.EasyCallMapper;
+import com.fs.company.param.ExecutionContext;
+import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
+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.time.LocalDateTime;
+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;
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private EasyCallMapper easyCallMapper;
+
+    @Autowired
+    private CompanyAiWorkflowExecMapper currentExecutionMapper;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+
+    @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();
+
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(req.getUuid());
+        String callBackUuid = UUID.randomUUID().toString();
+        CompanyAiWorkflowExec record = currentExecutionMapper.selectByWorkflowInstanceId(req.getWorkflowInstanceId());
+        try {
+            Map<String, Object> variablesMap;
+            if (record.getVariables() == null || record.getVariables().isEmpty()) {
+                variablesMap = new HashMap<>();
+            } else {
+                variablesMap = objectMapper.readValue(record.getVariables(), new TypeReference<Map<String, Object>>() {});
+            }
+            variablesMap.put("callBackUuid", callBackUuid);
+            record.setVariables(objectMapper.writeValueAsString(variablesMap));
+
+        } catch (Exception e) {
+            log.error("反序列化上下文变量失败", e);
+        }
+
+
+        CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone = new CompanyVoiceRoboticCallLogCallphone();
+        companyVoiceRoboticCallLogCallphone.setCallbackUuid(callBackUuid);
+        companyVoiceRoboticCallLogCallphone.setRoboticId(req.getRoboticId());
+        companyVoiceRoboticCallLogCallphone.setCallerId(null);
+        companyVoiceRoboticCallLogCallphone.setRunTime(null);
+        companyVoiceRoboticCallLogCallphone.setRunParam(null);
+        companyVoiceRoboticCallLogCallphone.setResult(null);
+        companyVoiceRoboticCallLogCallphone.setStatus(req.getStatus());
+        companyVoiceRoboticCallLogCallphone.setRecordPath(callPhoneRes.getRecordServerUrl());
+        companyVoiceRoboticCallLogCallphone.setContentList(callPhoneRes.getDialogue());
+        companyVoiceRoboticCallLogCallphone.setCallerNum(callPhoneRes.getTelephone());
+        companyVoiceRoboticCallLogCallphone.setCalleeNum(callPhoneRes.getCallerNumber());
+        companyVoiceRoboticCallLogCallphone.setUuid(req.getUuid());
+        companyVoiceRoboticCallLogCallphone.setCallCreateTime(callPhoneRes.getCalloutTime());
+        companyVoiceRoboticCallLogCallphone.setCallAnswerTime(callPhoneRes.getConnectedTime());
+        companyVoiceRoboticCallLogCallphone.setIntention(callPhoneRes.getIntent());
+        companyVoiceRoboticCallLogCallphone.setCompanyId(req.getCompanyId());
+        companyVoiceRoboticCallLogCallphone.setCompanyUserId(req.getCompanyUserId());
+        companyVoiceRoboticCallLogCallphone.setCallTime(Long.valueOf(callPhoneRes.getTimeLen()));
+        companyVoiceRoboticCallLogCallphone.setCost(callPhoneRes.getTotalCost());
+        companyVoiceRoboticCallLogCallphone.setCallType(Integer.valueOf(callType));
+
+
+        return companyVoiceRoboticCallLogCallphoneMapper.insertCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLogCallphone);
+    }
+
+
+}

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

+ 38 - 0
fs-service/src/main/java/com/fs/aicall/domain/CcTtsAliyun.java

@@ -0,0 +1,38 @@
+package com.fs.aicall.domain;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 音色表对象 cc_tts_aliyun(EASYCALL 数据源)
+ *
+ * @author fs
+ */
+@Data
+@Accessors(chain = true)
+public class CcTtsAliyun implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Integer id;
+
+    /** tts发音人名称 */
+    private String voiceName;
+
+    /** tts发音人代码(speaker_id) */
+    private String voiceCode;
+
+    /** 是否启用 */
+    private Integer voiceEnabled;
+
+    /** 声音源: aliyun_tts、doubao_vcl_tts 等 */
+    private String voiceSource;
+
+    /** 优先级 */
+    private Integer priority;
+
+    /** 供应商: aliyun、doubao 等 */
+    private String provider;
+}

+ 57 - 0
fs-service/src/main/java/com/fs/aicall/mapper/CcTtsAliyunMapper.java

@@ -0,0 +1,57 @@
+package com.fs.aicall.mapper;
+
+import com.fs.aicall.domain.CcTtsAliyun;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+
+import java.util.List;
+
+/**
+ * 音色表 Mapper 接口(EASYCALL 数据源)
+ *
+ * @author fs
+ */
+public interface CcTtsAliyunMapper {
+
+    /**
+     * 根据主键查询音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    CcTtsAliyun selectCcTtsAliyunById(Integer id);
+
+    /**
+     * 根据音色代码查询
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    CcTtsAliyun selectCcTtsAliyunByVoiceCode(String voiceCode);
+
+    /**
+     * 查询音色列表
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<CcTtsAliyun> selectCcTtsAliyunList(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 新增音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int insertCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 修改音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int updateCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 删除音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteCcTtsAliyunById(Integer id);
+
+    /**
+     * 批量删除音色
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    int deleteCcTtsAliyunByIds(String ids);
+}

+ 48 - 0
fs-service/src/main/java/com/fs/aicall/service/ICcTtsAliyunService.java

@@ -0,0 +1,48 @@
+package com.fs.aicall.service;
+
+import com.fs.aicall.domain.CcTtsAliyun;
+
+import java.util.List;
+
+/**
+ * 音色表 Service 接口(EASYCALL 数据源)
+ *
+ * @author fs
+ */
+public interface ICcTtsAliyunService {
+
+    /**
+     * 根据主键查询音色
+     */
+    CcTtsAliyun selectCcTtsAliyunById(Integer id);
+
+    /**
+     * 根据音色代码查询
+     */
+    CcTtsAliyun selectCcTtsAliyunByVoiceCode(String voiceCode);
+
+    /**
+     * 查询音色列表
+     */
+    List<CcTtsAliyun> selectCcTtsAliyunList(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 新增音色
+     */
+    int insertCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 修改音色
+     */
+    int updateCcTtsAliyun(CcTtsAliyun ccTtsAliyun);
+
+    /**
+     * 删除音色
+     */
+    int deleteCcTtsAliyunById(Integer id);
+
+    /**
+     * 批量删除音色
+     */
+    int deleteCcTtsAliyunByIds(String ids);
+}

+ 56 - 0
fs-service/src/main/java/com/fs/aicall/service/impl/CcTtsAliyunServiceImpl.java

@@ -0,0 +1,56 @@
+package com.fs.aicall.service.impl;
+
+import com.fs.aicall.domain.CcTtsAliyun;
+import com.fs.aicall.mapper.CcTtsAliyunMapper;
+import com.fs.aicall.service.ICcTtsAliyunService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 音色表 Service 实现(EASYCALL 数据源)
+ *
+ * @author fs
+ */
+@Service
+public class CcTtsAliyunServiceImpl implements ICcTtsAliyunService {
+
+    @Autowired
+    private CcTtsAliyunMapper ccTtsAliyunMapper;
+
+    @Override
+    public CcTtsAliyun selectCcTtsAliyunById(Integer id) {
+        return ccTtsAliyunMapper.selectCcTtsAliyunById(id);
+    }
+
+    @Override
+    public CcTtsAliyun selectCcTtsAliyunByVoiceCode(String voiceCode) {
+        return ccTtsAliyunMapper.selectCcTtsAliyunByVoiceCode(voiceCode);
+    }
+
+    @Override
+    public List<CcTtsAliyun> selectCcTtsAliyunList(CcTtsAliyun ccTtsAliyun) {
+        return ccTtsAliyunMapper.selectCcTtsAliyunList(ccTtsAliyun);
+    }
+
+    @Override
+    public int insertCcTtsAliyun(CcTtsAliyun ccTtsAliyun) {
+        return ccTtsAliyunMapper.insertCcTtsAliyun(ccTtsAliyun);
+    }
+
+    @Override
+    public int updateCcTtsAliyun(CcTtsAliyun ccTtsAliyun) {
+        return ccTtsAliyunMapper.updateCcTtsAliyun(ccTtsAliyun);
+    }
+
+    @Override
+    public int deleteCcTtsAliyunById(Integer id) {
+        return ccTtsAliyunMapper.deleteCcTtsAliyunById(id);
+    }
+
+    @Override
+    public int deleteCcTtsAliyunByIds(String ids) {
+        return ccTtsAliyunMapper.deleteCcTtsAliyunByIds(ids);
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/app/service/AppPayService.java

@@ -0,0 +1,12 @@
+package com.fs.app.service;
+
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
+
+public interface AppPayService {
+
+
+    /**
+     * 微信支付回调
+     */
+    String wxNotify(WxPayOrderNotifyResult result);
+}

+ 63 - 0
fs-service/src/main/java/com/fs/app/service/impl/AppPayServiceImpl.java

@@ -0,0 +1,63 @@
+package com.fs.app.service.impl;
+
+import com.fs.app.service.AppPayService;
+import com.fs.his.service.IFsInquiryOrderService;
+import com.fs.his.service.IFsPackageOrderService;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.system.mapper.SysConfigMapper;
+import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class AppPayServiceImpl implements AppPayService {
+
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private IFsInquiryOrderService inquiryOrderService;
+    @Lazy
+    @Autowired
+    private IFsStoreOrderScrmService storeOrderService;
+
+    @Lazy
+    @Autowired
+    private ILiveOrderService liveOrderService;
+    @Autowired
+    private IFsPackageOrderService packageOrderService;
+
+    @Override
+    public String wxNotify(WxPayOrderNotifyResult result) {
+        log.info("微信回调参数: {}", result);
+        if (!"SUCCESS".equals(result.getReturnCode())){
+            return WxPayNotifyResponse.success("微信回调失败");
+        }
+
+        if (!"SUCCESS".equals(result.getResultCode())){
+            return WxPayNotifyResponse.success("交易失败");
+        }
+
+        String[] tradeNoArr = result.getOutTradeNo().split("-");
+        switch (tradeNoArr[0]) {
+            case "inquiry":
+                inquiryOrderService.payConfirm("", tradeNoArr[1],"","",1,result.getTransactionId(),"");
+                break;
+            case "store":
+                storeOrderService.payConfirm(1, null,tradeNoArr[1],"",result.getTransactionId(),"");
+
+            case "live":
+                liveOrderService.payConfirm(1, null,tradeNoArr[1],"",result.getTransactionId(),"");
+                break;
+            case "package":
+                packageOrderService.payConfirm("", tradeNoArr[1],"","",1,result.getTransactionId(),"");
+                break;
+        }
+        return WxPayNotifyResponse.success("OK");
+    }
+
+}

+ 64 - 0
fs-service/src/main/java/com/fs/app/service/param/FsImMsgSendLogRequest.java

@@ -0,0 +1,64 @@
+package com.fs.app.service.param;
+
+import com.fs.common.core.page.PageDomain;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * OpenIM 消息发送记录请求参数
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsImMsgSendLogRequest extends PageDomain {
+
+    /** 销售 ID */
+    private Long companyUserId;
+
+    /** 公司 ID */
+    private Long companyId;
+
+    /** 课程 ID */
+    private Long courseId;
+
+    /** 课程名称 */
+    private String courseName;
+
+    /** 视频 ID */
+    private Long videoId;
+
+    /** 视频标题 */
+    private String videoName;
+
+    /** 发送内容 */
+    private String sendTitle;
+
+    /** 预计发送开始时间 */
+    private String planSendStartTime;
+
+    /** 预计发送结束时间 */
+    private String planSendEndTime;
+
+    /** 发送类型(1-定时;2-实时) */
+    private Integer sendType;
+
+    /** 发送方式(1-APP;2-销售后台) */
+    private Integer sendMode;
+
+    /** 发送状态(1-已发送;2-待发送) */
+    private Integer sendStatus;
+
+    /** 执行状态 执行状态,0-正常;1-失败*/
+    private Integer status;
+
+    /** 消息类型(1-发课;2-催课) */
+    private Integer msgType;
+
+    /** 项目 ID */
+    private Long projectId;
+
+    /** 创建开始时间 */
+    private String createTimeStartTime;
+
+    /** 创建结束时间 */
+    private String createTimeEndTime;
+}

+ 78 - 0
fs-service/src/main/java/com/fs/app/service/param/FsImMsgSendLogResponse.java

@@ -0,0 +1,78 @@
+package com.fs.app.service.param;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * OpenIM 消息发送记录响应参数
+ */
+@Data
+public class FsImMsgSendLogResponse {
+
+    /** 发送记录 id */
+    private Long logId;
+
+    /** 用户 ID(从详情表获取) */
+    private Long userId;
+
+    /** 销售 id(发送人 id) */
+    private Long companyUserId;
+
+    /** 公司 id */
+    private Long companyId;
+
+    /** 课程 id */
+    private Long courseId;
+
+    /** 课程名称 */
+    private String courseName;
+
+    /** 视频 id */
+    private Long videoId;
+
+    /** 视频标题 */
+    private String videoName;
+
+    /** 项目 ID */
+    private Long projectId;
+
+    /** 发送内容 */
+    private String sendTitle;
+
+    /** 预计发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date planSendTime;
+
+    /** 发送类型,1-定时;2-实时 */
+    private Integer sendType;
+
+    /** 发送方式,1-app;2-销售后台 */
+    private Integer sendMode;
+
+    /** 是否催课,1-是;0-否 */
+    private Boolean isUrgeCourse;
+
+    /** 消息类型,1-发课;2-催课 */
+    private Integer msgType;
+
+    /** 发送状态,1-已发送;2-待发送 */
+    private Integer sendStatus;
+
+    /** 执行状态,0-正常;1-失败 */
+    private Integer status;
+
+    /** 重试次数 */
+    private Integer count;
+
+    /** 执行结果 */
+    private String resultMessage;
+
+    /** 异常信息 */
+    private String exceptionInfo;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/app/service/param/FsImMsgSendLogStatisticsResponse.java

@@ -0,0 +1,20 @@
+package com.fs.app.service.param;
+
+import lombok.Data;
+
+/**
+ * OpenIM 消息发送记录响应参数
+ */
+@Data
+public class FsImMsgSendLogStatisticsResponse {
+    // 总数
+    private Long total;
+    // 已发送
+    private Long sent;
+    // 等待中
+    private Long pending;
+    // 异常
+    private Long failed;
+
+
+}

+ 44 - 9
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -14,10 +14,15 @@ import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsSendItemVO;
 import com.fs.common.vo.SmsSendVO;
 import com.fs.company.domain.*;
+import com.fs.company.enums.BusinessTypeEnum;
+import com.fs.company.enums.TargetTypeEnum;
+import com.fs.company.param.CompanyVoiceRoboticCallBlacklistCheckParam;
 import com.fs.company.service.ICompanySmsLogsService;
 import com.fs.company.service.ICompanySmsService;
 import com.fs.company.service.ICompanySmsTempService;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.company.service.impl.CompanyVoiceRoboticCallBlacklistServiceImpl;
+import com.fs.company.vo.CompanyVoiceRoboticCallBlacklistCheckVO;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.crm.param.SmsSendParam;
@@ -51,8 +56,10 @@ import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 import com.fs.company.service.ICompanySmsCommonLogsService;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 
 @Service
 @Slf4j
@@ -96,6 +103,12 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     private QwSopSmsLogsMapper qwSopSmsLogsMapper;
 
+    @Autowired
+    private CompanyVoiceRoboticCallBlacklistServiceImpl companyVoiceRoboticCallBlacklistService;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
     @Override
     public R sendTSms(String mobile, String code) {
 //        try{
@@ -914,7 +927,10 @@ public class SmsServiceImpl implements ISmsService
         }
     }
 
-    private static ConcurrentHashMap<Long, Object> smsCompanyLock = new ConcurrentHashMap();
+    /**
+     * 短信扣减分布式锁key前缀
+     */
+    private static final String SMS_SUB_LOCK_PREFIX = "SMS_SUB_LOCK:";
 
     /**
      * copy一份 batchSmsOp
@@ -924,6 +940,14 @@ public class SmsServiceImpl implements ISmsService
 //    @Async
     public void batchSmsOp4AiSend(CompanySmsTemp temp, SmsSendBatchParam param){
         CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+        CompanyVoiceRoboticCallBlacklistCheckParam companyVoiceRoboticCallBlacklistCheckParam = new CompanyVoiceRoboticCallBlacklistCheckParam();
+        companyVoiceRoboticCallBlacklistCheckParam.setCompanyId(param.getCompanyId());
+        companyVoiceRoboticCallBlacklistCheckParam.setBusinessType(BusinessTypeEnum.SMS.getCode());
+        companyVoiceRoboticCallBlacklistCheckParam.setTargetValue(companyUser.getPhonenumber());
+        CompanyVoiceRoboticCallBlacklistCheckVO companyVoiceRoboticCallBlacklistCheckVO = companyVoiceRoboticCallBlacklistService.checkBlacklist(companyVoiceRoboticCallBlacklistCheckParam);
+        if (!companyVoiceRoboticCallBlacklistCheckVO.getPass()){
+            throw new RuntimeException("黑名单校验未通过");
+        }
         for(Long id:param.getCustomerIds()){
             CrmCustomer crmCustomer=crmCustomerService.selectCrmCustomerById(id);
             String content="";
@@ -986,12 +1010,17 @@ public class SmsServiceImpl implements ISmsService
                             }
                             logs.setNumber(counts);
                             smsLogsService.insertCompanySmsLogs(logs);
-                            Object lock = smsCompanyLock.computeIfAbsent(logs.getCompanyId(), k -> new Object());
-                            //任务调用时可能存在并发情况 这里优化一下扣减时条数准确
-                            synchronized (lock) {
+                            // 使用Redisson分布式锁,按公司ID级别锁定,确保集群环境下扣减准确
+                            String lockKey = SMS_SUB_LOCK_PREFIX + logs.getCompanyId();
+                            RLock lock = redissonClient.getLock(lockKey);
+                            try {
+                                lock.lock(30, TimeUnit.SECONDS);
                                 companySmsService.subCompanySms(logs.getCompanyId(), logs.getNumber());
+                            } finally {
+                                if (lock.isHeldByCurrentThread()) {
+                                    lock.unlock();
+                                }
                             }
-//                            companySmsService.subCompanySms(logs.getCompanyId(),logs.getNumber());
                             resultCount++;
                         }
                     }
@@ -1028,10 +1057,16 @@ public class SmsServiceImpl implements ISmsService
                     if (sendSmsReturn.getResult()!=null&&sendSmsReturn.getResult().equals("0")){
                         logs.setMid(sendSmsReturn.getMsgid());
                         smsLogsService.insertCompanySmsLogs(logs);
-//                        companySmsService.subCompanySms(logs.getCompanyId(),logs.getNumber());
-                        Object lock = smsCompanyLock.computeIfAbsent(logs.getCompanyId(), k -> new Object());
-                        synchronized (lock) {
+                        // 使用Redisson分布式锁,按公司ID级别锁定,确保集群环境下扣减准确
+                        String lockKey = SMS_SUB_LOCK_PREFIX + logs.getCompanyId();
+                        RLock lock = redissonClient.getLock(lockKey);
+                        try {
+                            lock.lock(30, TimeUnit.SECONDS);
                             companySmsService.subCompanySms(logs.getCompanyId(), logs.getNumber());
+                        } finally {
+                            if (lock.isHeldByCurrentThread()) {
+                                lock.unlock();
+                            }
                         }
                     }
                 }

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

@@ -143,4 +143,7 @@ public class Company extends BaseEntity
 
     @TableField(exist = false)
     private List<Long> ids;
+
+    @TableField(exist = false)
+    private List<Long> showGatewayIds;
 }

+ 22 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyBindGateway.java

@@ -0,0 +1,22 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/1 16:22
+ * @description
+ */
+@Data
+public class CompanyBindGateway {
+    private  Integer id;
+    //网关线路id
+    private Long gatewayId;
+    //公司id
+    private Long companyId;
+    //创建时间
+    private Date createTime;
+
+}

+ 37 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallBlackLog.java

@@ -0,0 +1,37 @@
+package com.fs.company.domain;
+
+
+import com.fs.common.core.domain.BaseEntity;
+
+import java.util.Date;
+
+/**
+ * 黑名单对象 company_voice_blacklist
+ * @author ZhuanZ(无密码)
+ */
+public class CompanyVoiceRoboticCallBlackLog extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    private Long companyId;
+
+    private Long roboticId;
+
+    private Long customerId;
+
+    private String externalUserId;
+
+    private String phone;
+
+    private String blockReason;
+
+    private Long blacklistId;
+
+    private String requestData;
+
+    private Date createTime;
+
+
+}

+ 85 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallBlacklist.java

@@ -0,0 +1,85 @@
+package com.fs.company.domain;
+
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+
+/**
+ * 黑名单对象 company_voice_blacklist
+ * @author ZhuanZ(无密码)
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class CompanyVoiceRoboticCallBlacklist extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    /**
+     * 业务类型
+     */
+    private String businessType;
+
+    /**
+     * 1手机号 2客户ID 3企微客户ID
+     */
+    private Integer targetType;
+
+    /**
+     * 对象值
+     */
+    private String targetValue;
+
+    /**
+     * 公司ID
+     */
+    private Long companyId;
+
+    /**
+     * 拉黑原因
+     */
+    private String reason;
+
+    /**
+     * 来源:1手工添加 2系统
+     */
+    private Integer source;
+
+    /**
+     * 状态:1生效 0禁用
+     */
+    private Integer status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    private String createBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    private String updateBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /**
+     * 逻辑删除:0否 1是
+     */
+    private Integer deleted;
+
+    /**
+     * 只看全局黑名单
+     */
+    private Boolean onlyGlobal;
+
+}

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

@@ -103,6 +103,9 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @Excel(name = "花费金额")
     private BigDecimal cost;
 
+    @Excel(name = "外呼类型")
+    private Integer callType;
+
     @TableField(exist = false)
     private String companyName;
 

+ 58 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowEdgeVersion.java

@@ -0,0 +1,58 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI工作流连线对象 fs_ai_workflow_edge版本表
+ */
+@Data
+public class CompanyWorkflowEdgeVersion implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    /**
+     * 版本ID
+     */
+    private Long versionId;
+
+    /**
+     * 工作流ID
+     */
+    private Long workflowId;
+
+    /**
+     * 原连线ID
+     */
+    private Long edgeId;
+
+    private String edgeKey;
+
+    private String edgeLabel;
+
+    private String sourceNodeKey;
+
+    private String targetNodeKey;
+
+    private String sourceAnchor;
+
+    private String targetAnchor;
+
+    private String edgeType;
+
+    private String edgeColor;
+
+    private String conditionExpr;
+
+    private Integer sortOrder;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 67 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowNodeVersion.java

@@ -0,0 +1,67 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI工作流节点对象 fs_ai_workflow_node
+ */
+@Data
+public class CompanyWorkflowNodeVersion implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    /**
+     * 版本ID
+     */
+    private Long versionId;
+
+    /**
+     * 工作流ID
+     */
+    private Long workflowId;
+
+    /**
+     * 原节点ID
+     */
+    private Long nodeId;
+
+    private String nodeKey;
+
+    private String nodeName;
+
+    private String nodeType;
+
+    private String nodeIcon;
+
+    private String nodeColor;
+
+    private Integer posX;
+
+    private Integer posY;
+
+    private Integer width;
+
+    private Integer height;
+
+    private String nodeConfig;
+
+    private Integer sortOrder;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /**
+     * 语音URL
+     */
+    private String voiceUrl;
+}

+ 94 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowVersion.java

@@ -0,0 +1,94 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class CompanyWorkflowVersion {
+
+    /**
+     * 版本ID
+     */
+    private Long versionId;
+
+    /**
+     * 工作流ID
+     */
+    private Long workflowId;
+
+    /**
+     * 版本号
+     */
+    private Integer versionNo;
+
+    /**
+     * 工作流名称
+     */
+    private String workflowName;
+
+    /**
+     * 工作流描述
+     */
+    private String workflowDesc;
+
+    /**
+     * 工作流类型
+     */
+    private Integer workflowType;
+
+    /**
+     * 状态 0禁用 1启用
+     */
+    private Integer status;
+
+    /**
+     * 画布JSON
+     */
+    private String canvasData;
+
+    /**
+     * 开始节点key
+     */
+    private String startNodeKey;
+
+    /**
+     * 结束节点key
+     */
+    private String endNodeKey;
+
+    /**
+     * 公司用户ID
+     */
+    private Long companyUserId;
+
+    /**
+     * 公司ID
+     */
+    private Integer companyId;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 快照时间
+     */
+    private Date snapshotTime;
+
+    /**
+     * 快照人
+     */
+    private String snapshotBy;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+}

+ 83 - 0
fs-service/src/main/java/com/fs/company/enums/BusinessTypeEnum.java

@@ -0,0 +1,83 @@
+package com.fs.company.enums;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 业务类型枚举
+ *
+ * @author ZhuanZ
+ */
+public enum BusinessTypeEnum {
+
+    /**
+     * 外呼
+     */
+    CALL("CALL", "外呼"),
+
+    /**
+     * 加微
+     */
+    ADD_WX("ADD_WX", "加微"),
+
+    /**
+     * 短信
+     */
+    SMS("SMS", "短信");
+
+    private final String code;
+    private final String desc;
+
+    BusinessTypeEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    /**
+     * 根据 code 获取枚举
+     */
+    public static BusinessTypeEnum fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (BusinessTypeEnum item : values()) {
+            if (item.code.equalsIgnoreCase(code)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 校验是否为合法业务类型
+     */
+    public static boolean isValid(String code) {
+        return fromCode(code) != null;
+    }
+
+    /**
+     * 获取所有 code 列表
+     */
+    public static List<String> getCodes() {
+        return Arrays.stream(values())
+                .map(BusinessTypeEnum::getCode)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 判断是否相等(忽略大小写)
+     */
+    public boolean equalsCode(String code) {
+        return this.code.equalsIgnoreCase(code);
+    }
+
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio