|
@@ -16,6 +16,8 @@ import com.fs.common.exception.ServiceException;
|
|
|
import com.fs.common.utils.SecurityUtils;
|
|
import com.fs.common.utils.SecurityUtils;
|
|
|
import com.fs.common.utils.StringUtils;
|
|
import com.fs.common.utils.StringUtils;
|
|
|
import com.fs.common.utils.poi.ExcelUtil;
|
|
import com.fs.common.utils.poi.ExcelUtil;
|
|
|
|
|
+import com.fs.company.domain.CrmCustomerCallLog;
|
|
|
|
|
+import com.fs.company.mapper.CrmCustomerCallLogMapper;
|
|
|
import com.fs.company.mapper.EasyCallMapper;
|
|
import com.fs.company.mapper.EasyCallMapper;
|
|
|
import com.fs.company.vo.easycall.EasyCallOutBoundVO;
|
|
import com.fs.company.vo.easycall.EasyCallOutBoundVO;
|
|
|
import com.fs.framework.datasource.TenantDataSourceManager;
|
|
import com.fs.framework.datasource.TenantDataSourceManager;
|
|
@@ -34,6 +36,8 @@ import java.util.Base64;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* aiSIP手动外呼通话记录Controller
|
|
* aiSIP手动外呼通话记录Controller
|
|
@@ -54,6 +58,8 @@ public class AiSipCallOutboundCdrController extends BaseController
|
|
|
TenantDataSourceManager tenantDataSourceManager;
|
|
TenantDataSourceManager tenantDataSourceManager;
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private TokenService tokenService;
|
|
private TokenService tokenService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private CrmCustomerCallLogMapper crmCustomerCallLogMapper;
|
|
|
|
|
|
|
|
/** XOR加密公钥(与 his_java PhoneUtil.PUBLIC_KEY_STR 保持一致) */
|
|
/** XOR加密公钥(与 his_java PhoneUtil.PUBLIC_KEY_STR 保持一致) */
|
|
|
private static final String XOR_KEY = "ylrz112233";
|
|
private static final String XOR_KEY = "ylrz112233";
|
|
@@ -175,11 +181,18 @@ public class AiSipCallOutboundCdrController extends BaseController
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ /** 同步重试最大次数 */
|
|
|
|
|
+ private static final int SYNC_RETRY_MAX_TIMES = 3;
|
|
|
|
|
+ /** 同步重试间隔(秒) */
|
|
|
|
|
+ private static final long SYNC_RETRY_INTERVAL_SECONDS = 10L;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 同步aiSIP外呼通话记录
|
|
* 同步aiSIP外呼通话记录
|
|
|
* 说明:
|
|
* 说明:
|
|
|
* 1. 如果有 workflowInstanceId 和 roboticId,走原来的机器人/工作流外呼记录
|
|
* 1. 如果有 workflowInstanceId 和 roboticId,走原来的机器人/工作流外呼记录
|
|
|
* 2. 如果没有 workflowInstanceId 和 roboticId,走用户手动外呼记录
|
|
* 2. 如果没有 workflowInstanceId 和 roboticId,走用户手动外呼记录
|
|
|
|
|
+ * 3. 如果对方通话记录尚未写入,立即返回"正在同步录音中",后台异步重试3次(每次间隔10秒)
|
|
|
|
|
+ * 4. 3次重试后仍无数据,则用请求参数写入一条仅含基础信息的CrmCustomerCallLog(不含通话数据),后续可通过uuid手动同步
|
|
|
*/
|
|
*/
|
|
|
@PostMapping("/syncByUuid")
|
|
@PostMapping("/syncByUuid")
|
|
|
public AjaxResult syncByUuid(@RequestBody ApiCallRecordByUuidQueryParams req) {
|
|
public AjaxResult syncByUuid(@RequestBody ApiCallRecordByUuidQueryParams req) {
|
|
@@ -195,10 +208,13 @@ public class AiSipCallOutboundCdrController extends BaseController
|
|
|
//获取租户id
|
|
//获取租户id
|
|
|
req.setTenantId(getTenantId());
|
|
req.setTenantId(getTenantId());
|
|
|
EasyCallOutBoundVO callPhoneRes = easyCallMapper.getOutBoundInfoByUuid(req.getUuid());
|
|
EasyCallOutBoundVO callPhoneRes = easyCallMapper.getOutBoundInfoByUuid(req.getUuid());
|
|
|
|
|
+ tenantDataSourceManager.ensureSwitchByTenantId(SecurityUtils.getTenantId());
|
|
|
if (ObjectUtil.isEmpty(callPhoneRes)) {
|
|
if (ObjectUtil.isEmpty(callPhoneRes)) {
|
|
|
- return AjaxResult.error("未同步到对应通话记录");
|
|
|
|
|
|
|
+ // 对方通话记录尚未写入,异步重试
|
|
|
|
|
+ log.info("syncByUuid uuid={} 首次未查询到通话记录,启动异步重试", req.getUuid());
|
|
|
|
|
+ asyncRetrySyncByUuid(req);
|
|
|
|
|
+ return AjaxResult.success("正在同步录音中");
|
|
|
}
|
|
}
|
|
|
- tenantDataSourceManager.ensureSwitchByTenantId(SecurityUtils.getTenantId());
|
|
|
|
|
int rows;
|
|
int rows;
|
|
|
if (req.getWorkflowInstanceId() != null && req.getRoboticId() != null) {
|
|
if (req.getWorkflowInstanceId() != null && req.getRoboticId() != null) {
|
|
|
// 工作流外呼保存逻辑
|
|
// 工作流外呼保存逻辑
|
|
@@ -216,6 +232,88 @@ public class AiSipCallOutboundCdrController extends BaseController
|
|
|
return AjaxResult.error("未查到对应通话记录或同步失败");
|
|
return AjaxResult.error("未查到对应通话记录或同步失败");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 异步重试同步通话记录
|
|
|
|
|
+ * 最多重试3次,每次间隔10秒;若3次均未查到数据,则用请求参数写入一条仅含基础信息的CrmCustomerCallLog
|
|
|
|
|
+ */
|
|
|
|
|
+ private void asyncRetrySyncByUuid(ApiCallRecordByUuidQueryParams req) {
|
|
|
|
|
+ Long tenantId = req.getTenantId();
|
|
|
|
|
+ CompletableFuture.runAsync(() -> {
|
|
|
|
|
+ EasyCallOutBoundVO callPhoneRes = null;
|
|
|
|
|
+ for (int i = 1; i <= SYNC_RETRY_MAX_TIMES; i++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ TimeUnit.SECONDS.sleep(SYNC_RETRY_INTERVAL_SECONDS);
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ log.error("syncByUuid uuid={} 重试等待被中断", req.getUuid(), e);
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("syncByUuid uuid={} 第{}次重试查询通话记录", req.getUuid(), i);
|
|
|
|
|
+ try {
|
|
|
|
|
+ callPhoneRes = easyCallMapper.getOutBoundInfoByUuid(req.getUuid());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("syncByUuid uuid={} 第{}次重试查询异常", req.getUuid(), i, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ObjectUtil.isNotEmpty(callPhoneRes)) {
|
|
|
|
|
+ log.info("syncByUuid uuid={} 第{}次重试查询到通话记录", req.getUuid(), i);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 切换回租户数据源
|
|
|
|
|
+ tenantDataSourceManager.ensureSwitchByTenantId(tenantId);
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (ObjectUtil.isNotEmpty(callPhoneRes)) {
|
|
|
|
|
+ // 重试成功,走正常同步逻辑
|
|
|
|
|
+ if (req.getWorkflowInstanceId() != null && req.getRoboticId() != null) {
|
|
|
|
|
+ aiSipCallOutboundCdrService.syncByUuid(req, callPhoneRes);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (req.getCustomerId() != null) {
|
|
|
|
|
+ aiSipCallOutboundCdrService.syncCrmCustomerCallLogByUuid(req, callPhoneRes);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.warn("syncByUuid uuid={} 重试成功但客户ID为空,跳过手动外呼同步", req.getUuid());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("syncByUuid uuid={} 异步重试同步完成", req.getUuid());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 3次重试均无数据,用请求参数写入仅含基础信息的CrmCustomerCallLog(不含通话详细数据)
|
|
|
|
|
+ log.warn("syncByUuid uuid={} 重试{}次后仍未查到通话记录,写入基础记录待后续手动同步", req.getUuid(), SYNC_RETRY_MAX_TIMES);
|
|
|
|
|
+ insertBasicCrmCustomerCallLog(req);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("syncByUuid uuid={} 异步重试同步处理异常", req.getUuid(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 用请求参数写入仅含基础信息的CrmCustomerCallLog(不含通话数据:录音、内容、时长、费用等)
|
|
|
|
|
+ * 后续可通过uuid手动同步补充通话数据
|
|
|
|
|
+ */
|
|
|
|
|
+ private void insertBasicCrmCustomerCallLog(ApiCallRecordByUuidQueryParams req) {
|
|
|
|
|
+ CrmCustomerCallLog callLog = new CrmCustomerCallLog();
|
|
|
|
|
+ callLog.setUuid(req.getUuid());
|
|
|
|
|
+ callLog.setStatus(req.getStatus());
|
|
|
|
|
+ callLog.setCompanyId(req.getCompanyId());
|
|
|
|
|
+ callLog.setCompanyUserId(req.getCompanyUserId());
|
|
|
|
|
+ callLog.setCustomerId(req.getCustomerId());
|
|
|
|
|
+ callLog.setIntention(req.getIntent());
|
|
|
|
|
+ callLog.setCreateTime(new Date());
|
|
|
|
|
+ callLog.setRunTime(new Date());
|
|
|
|
|
+ if (StringUtils.isNotBlank(req.getCallType())) {
|
|
|
|
|
+ callLog.setCallType(Integer.valueOf(req.getCallType()));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ callLog.setCallType(3); // 默认03人工外呼
|
|
|
|
|
+ }
|
|
|
|
|
+ // 不写通话相关数据:recordPath、contentList、callerNum、calleeNum、callCreateTime、callAnswerTime、callTime、cost、billingMinute
|
|
|
|
|
+ // 后续可通过uuid手动同步补充
|
|
|
|
|
+ try {
|
|
|
|
|
+ crmCustomerCallLogMapper.insertCrmCustomerCallLog(callLog);
|
|
|
|
|
+ log.info("syncByUuid uuid={} 写入基础CrmCustomerCallLog记录成功", req.getUuid());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("syncByUuid uuid={} 写入基础CrmCustomerCallLog记录失败", req.getUuid(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public static Long getTenantId() {
|
|
public static Long getTenantId() {
|
|
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
|
if (auth == null || auth.getPrincipal() == null) {
|
|
if (auth == null || auth.getPrincipal() == null) {
|