|
|
@@ -1,43 +1,74 @@
|
|
|
package com.fs.aiSipCall.service.impl;
|
|
|
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.alibaba.fastjson.TypeReference;
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
import com.fs.aiSipCall.RemoteCommon;
|
|
|
+import com.fs.aiSipCall.domain.AiSipCallPhone;
|
|
|
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.IAiSipAutoCallRulesService;
|
|
|
import com.fs.aiSipCall.service.IAiSipCallPhoneService;
|
|
|
import com.fs.aiSipCall.service.IAiSipCallTaskService;
|
|
|
-import lombok.extern.slf4j.Slf4j;
|
|
|
+import com.fs.aiSipCall.utils.RemoteApiHelper;
|
|
|
+import com.fs.aiSipCall.utils.SipDESUtil;
|
|
|
+import com.fs.aiSipCall.utils.SipLogUtil;
|
|
|
+import com.fs.common.core.domain.AjaxResult;
|
|
|
+import com.fs.common.exception.ServiceException;
|
|
|
+import com.fs.his.domain.FsUser;
|
|
|
+import com.fs.his.service.IFsUserService;
|
|
|
+import com.fs.his.utils.PhoneUtil;
|
|
|
+import com.fs.sop.domain.SopUserLogsInfo;
|
|
|
+import com.google.common.cache.Cache;
|
|
|
+import com.google.common.cache.CacheBuilder;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.slf4j.Logger;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.context.annotation.Lazy;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.util.CollectionUtils;
|
|
|
|
|
|
-import java.util.HashMap;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Map;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.net.URLDecoder;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
* aiSIP外呼任务Service业务层处理
|
|
|
- *
|
|
|
+ *
|
|
|
* @author fs
|
|
|
* @date 2026-03-06
|
|
|
*/
|
|
|
-@Slf4j
|
|
|
@Service
|
|
|
public class AiSipCallTaskServiceImpl extends ServiceImpl<AiSipCallTaskMapper, AiSipCallTask> implements IAiSipCallTaskService {
|
|
|
|
|
|
+ private static final Logger log = SipLogUtil.createSipLogger(AiSipCallTaskServiceImpl.class);
|
|
|
+
|
|
|
+ /** 命中自动外呼屏蔽时段时的提示 */
|
|
|
+ private static final String SHIELD_TIME_FAIL_REASON = "当前处于自动外呼屏蔽时段,无法导入外呼";
|
|
|
|
|
|
@Autowired
|
|
|
private IAiSipCallPhoneService aiSipCallPhoneService;
|
|
|
+ @Autowired
|
|
|
+ private IAiSipAutoCallRulesService aiSipAutoCallRulesService;
|
|
|
+ /** 使用手动线路项目前缀 */
|
|
|
+ @Value(value = "${sip.call.manualGatewayPrefix:weizhi}")
|
|
|
+ private String manualGatewayPrefix;
|
|
|
+ @Lazy
|
|
|
+ @Autowired
|
|
|
+ private IFsUserService fsUserService;
|
|
|
|
|
|
/**
|
|
|
* 查询aiSIP外呼任务
|
|
|
- *
|
|
|
+ *
|
|
|
* @param batchId aiSIP外呼任务主键
|
|
|
* @return aiSIP外呼任务
|
|
|
*/
|
|
|
@@ -49,230 +80,949 @@ public class AiSipCallTaskServiceImpl extends ServiceImpl<AiSipCallTaskMapper, A
|
|
|
|
|
|
/**
|
|
|
* 查询aiSIP外呼任务列表
|
|
|
- *
|
|
|
+ *
|
|
|
* @param aiSipCallTask aiSIP外呼任务
|
|
|
* @return aiSIP外呼任务
|
|
|
*/
|
|
|
@Override
|
|
|
public List<AiSipCallTask> selectAiSipCallTaskList(AiSipCallTask aiSipCallTask)
|
|
|
{
|
|
|
- return baseMapper.selectAiSipCallTaskList(aiSipCallTask);
|
|
|
+ List<AiSipCallTask> list = baseMapper.selectAiSipCallTaskList(aiSipCallTask);
|
|
|
+ if (CollectionUtils.isEmpty(list)) {
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+ List<Long> remoteBatchIds = list.stream()
|
|
|
+ .map(AiSipCallTask::getRemoteBatchId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .distinct()
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ Map<Long, CallTaskStatModel> statModelMap = Collections.emptyMap();
|
|
|
+ if (!remoteBatchIds.isEmpty()) {
|
|
|
+ List<CallTaskStatModel> statModels = aiSipCallPhoneService.statByBatchIds(remoteBatchIds);
|
|
|
+ if (!CollectionUtils.isEmpty(statModels)) {
|
|
|
+ statModelMap = statModels.stream()
|
|
|
+ .filter(stat -> stat.getBatchId() != null)
|
|
|
+ .collect(Collectors.toMap(CallTaskStatModel::getBatchId, v -> v, (k1, k2) -> k1));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Map<Long, CallTaskStatModel> finalStatMap = statModelMap;
|
|
|
+ list.forEach(task -> fillTaskStat(task, finalStatMap.get(task.getRemoteBatchId())));
|
|
|
+ return list;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
/**
|
|
|
- * 新增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外呼任务失败:{}", "接口返回为空");
|
|
|
+ private void fillTaskStat(AiSipCallTask task, CallTaskStatModel statModel) {
|
|
|
+ if (statModel == null) {
|
|
|
+ task.setPhoneCount(0);
|
|
|
+ task.setCallCount(0);
|
|
|
+ task.setNoCallCount(0);
|
|
|
+ task.setConnectCount(0);
|
|
|
+ task.setNoConnectCount(0);
|
|
|
+ task.setRealConnectRate(0.00);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ int phoneCount = Optional.ofNullable(statModel.getPhoneCount()).orElse(0);
|
|
|
+ int callCount = Optional.ofNullable(statModel.getCallCount()).orElse(0);
|
|
|
+ int connectCount = Optional.ofNullable(statModel.getConnectCount()).orElse(0);
|
|
|
+ task.setPhoneCount(phoneCount);
|
|
|
+ task.setCallCount(callCount);
|
|
|
+ task.setNoCallCount(Math.max(phoneCount - callCount, 0));
|
|
|
+ task.setConnectCount(connectCount);
|
|
|
+ task.setNoConnectCount(Math.max(phoneCount - connectCount, 0));
|
|
|
+ if (phoneCount > 0) {
|
|
|
+ task.setRealConnectRate(BigDecimal.valueOf(connectCount * 100.0 / phoneCount)
|
|
|
+ .setScale(2, RoundingMode.HALF_UP).doubleValue());
|
|
|
+ } else {
|
|
|
+ task.setRealConnectRate(0.00);
|
|
|
}
|
|
|
- return 0;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 修改aiSIP外呼任务
|
|
|
- *
|
|
|
- * @param aiSipCallTask aiSIP外呼任务
|
|
|
- * @return 结果
|
|
|
- */
|
|
|
+
|
|
|
@Override
|
|
|
- public int updateAiSipCallTask(AiSipCallTask aiSipCallTask)
|
|
|
- {
|
|
|
+ public int insertAiSipCallTask(AiSipCallTask aiSipCallTask) {
|
|
|
+ Objects.requireNonNull(aiSipCallTask, "aiSipCallTask");
|
|
|
+ AiSipCallTask remoteSipCallTask = new AiSipCallTask();
|
|
|
+ BeanUtils.copyProperties(aiSipCallTask, remoteSipCallTask);
|
|
|
+ remoteSipCallTask.setBatchName(manualGatewayPrefix + "_" + aiSipCallTask.getBatchName());
|
|
|
+
|
|
|
+ JSONObject resp = RemoteApiHelper.parseSuccess(
|
|
|
+ RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.CREATE_TASK_API,
|
|
|
+ JSONObject.toJSONString(remoteSipCallTask)),
|
|
|
+ "新增aiSIP外呼任务"
|
|
|
+ );
|
|
|
+ Long remoteBatchId = Optional.ofNullable(RemoteApiHelper.toObject(resp.get("data"), JSONObject.class))
|
|
|
+ .map(obj -> obj.getLong("batchId"))
|
|
|
+ .orElseThrow(() -> new ServiceException("新增aiSIP外呼任务失败:远程返回batchId为空"));
|
|
|
+ aiSipCallTask.setRemoteBatchId(remoteBatchId);
|
|
|
+ processField(aiSipCallTask);
|
|
|
+ return baseMapper.insertAiSipCallTask(aiSipCallTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int updateAiSipCallTask(AiSipCallTask aiSipCallTask) {
|
|
|
+ Objects.requireNonNull(aiSipCallTask, "aiSipCallTask");
|
|
|
AiSipCallTask remoteTask = new AiSipCallTask();
|
|
|
- BeanUtils.copyProperties(aiSipCallTask,remoteTask);
|
|
|
+ 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外呼任务失败:{}", "接口返回为空");
|
|
|
+ if (StringUtils.isNotBlank(aiSipCallTask.getBatchName())
|
|
|
+ && !aiSipCallTask.getBatchName().startsWith(manualGatewayPrefix + "_")) {
|
|
|
+ remoteTask.setBatchName(manualGatewayPrefix + "_" + aiSipCallTask.getBatchName());
|
|
|
}
|
|
|
- return 0;
|
|
|
+
|
|
|
+ RemoteApiHelper.parseSuccess(
|
|
|
+ RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.EDIT_TASK_API,
|
|
|
+ JSONObject.toJSONString(remoteTask)),
|
|
|
+ "修改aiSIP外呼任务"
|
|
|
+ );
|
|
|
+ processField(aiSipCallTask);
|
|
|
+ return baseMapper.updateAiSipCallTask(aiSipCallTask);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 处理字段
|
|
|
- * @param aiSipCallTask 字段
|
|
|
*/
|
|
|
private void processField(AiSipCallTask aiSipCallTask) {
|
|
|
- if(aiSipCallTask.getAiTransferType().equals("extension")){
|
|
|
+ String transferType = StringUtils.defaultString(aiSipCallTask.getAiTransferType());
|
|
|
+ if ("extension".equals(transferType)) {
|
|
|
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());
|
|
|
+ } else if ("gateway".equals(transferType)) {
|
|
|
+ Map<String, String> map = new HashMap<>(2);
|
|
|
+ map.put("destNumber", aiSipCallTask.getAiTransferGatewayDestNumber());
|
|
|
+ map.put("gatewayId", aiSipCallTask.getAiTransferGatewayId());
|
|
|
aiSipCallTask.setAiTransferData(JSONObject.toJSONString(map));
|
|
|
- }else{
|
|
|
+ } else {
|
|
|
aiSipCallTask.setAiTransferData(aiSipCallTask.getAiTransferGroupId());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 批量删除aiSIP外呼任务
|
|
|
- *
|
|
|
- * @param batchIds 需要删除的aiSIP外呼任务主键
|
|
|
- * @return 结果
|
|
|
- */
|
|
|
@Override
|
|
|
- public int deleteAiSipCallTaskByBatchIds(Long[] batchIds)
|
|
|
- {
|
|
|
- return baseMapper.deleteAiSipCallTaskByBatchIds(batchIds);
|
|
|
+ public int deleteAiSipCallTaskByBatchIds(Long[] batchIds) {
|
|
|
+ if (batchIds == null || batchIds.length == 0) {
|
|
|
+ throw new ServiceException("删除aiSIP外呼任务失败:任务ID为空");
|
|
|
+ }
|
|
|
+ List<AiSipCallTask> aiSipCallTasks = baseMapper.selectAiSipCallTaskByBatchIds(Arrays.asList(batchIds));
|
|
|
+ if (CollectionUtils.isEmpty(aiSipCallTasks)) {
|
|
|
+ throw new ServiceException("删除aiSIP外呼任务失败:未找到该任务");
|
|
|
+ }
|
|
|
+ Long[] remoteBatchIds = aiSipCallTasks.stream()
|
|
|
+ .map(AiSipCallTask::getRemoteBatchId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .toArray(Long[]::new);
|
|
|
+ if (remoteBatchIds.length == 0) {
|
|
|
+ throw new ServiceException("删除aiSIP外呼任务失败:无有效远程任务ID");
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Long[]> paramMap = new HashMap<>(1);
|
|
|
+ paramMap.put("batchIds", remoteBatchIds);
|
|
|
+ RemoteApiHelper.parseSuccess(
|
|
|
+ RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.DEL_TASK_API,
|
|
|
+ JSONObject.toJSONString(paramMap)),
|
|
|
+ "删除aiSIP外呼任务"
|
|
|
+ );
|
|
|
+ aiSipCallPhoneService.logicalDeletionByBatchIds(batchIds);
|
|
|
+ return baseMapper.logicalDeletionByBatchIds(batchIds);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 删除aiSIP外呼任务信息
|
|
|
- *
|
|
|
- * @param batchId aiSIP外呼任务主键
|
|
|
- * @return 结果
|
|
|
- */
|
|
|
@Override
|
|
|
- public int deleteAiSipCallTaskByBatchId(Long batchId)
|
|
|
- {
|
|
|
+ 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;
|
|
|
+ AiSipCallTask ccCallTask = requireTask(batchId, "启动外呼");
|
|
|
+ RemoteApiHelper.parseSuccess(
|
|
|
+ RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.START_TASK_API
|
|
|
+ + "?batchId=" + ccCallTask.getRemoteBatchId()),
|
|
|
+ "启动外呼"
|
|
|
+ );
|
|
|
+ ccCallTask.setIfcall(1L);
|
|
|
+ ccCallTask.setExecuting(0L);
|
|
|
+ ccCallTask.setStopTime(0L);
|
|
|
+ baseMapper.updateAiSipCallTask(ccCallTask);
|
|
|
+ return 1;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public int stopTask(Long batchId) {
|
|
|
- AiSipCallTask ccCallTask = baseMapper.selectAiSipCallTaskByBatchId(batchId);
|
|
|
- if(null == ccCallTask){
|
|
|
- return 0;
|
|
|
+ AiSipCallTask ccCallTask = requireTask(batchId, "停止外呼");
|
|
|
+ RemoteApiHelper.parseSuccess(
|
|
|
+ RemoteCommon.sendGet(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.STOP_TASK_API
|
|
|
+ + "?batchId=" + ccCallTask.getRemoteBatchId()),
|
|
|
+ "停止外呼"
|
|
|
+ );
|
|
|
+ ccCallTask.setIfcall(0L);
|
|
|
+ ccCallTask.setExecuting(0L);
|
|
|
+ ccCallTask.setStopTime(System.currentTimeMillis());
|
|
|
+ baseMapper.updateAiSipCallTask(ccCallTask);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private AiSipCallTask requireTask(Long batchId, String apiDesc) {
|
|
|
+ if (batchId == null) {
|
|
|
+ throw new ServiceException(apiDesc + "失败:任务ID为空");
|
|
|
+ }
|
|
|
+ AiSipCallTask task = baseMapper.selectAiSipCallTaskByBatchId(batchId);
|
|
|
+ if (task == null) {
|
|
|
+ throw new ServiceException(apiDesc + "失败:任务不存在");
|
|
|
+ }
|
|
|
+ if (task.getRemoteBatchId() == null) {
|
|
|
+ throw new ServiceException(apiDesc + "失败:远程任务ID为空");
|
|
|
}
|
|
|
- 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 task;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int commonImportExcel(Long batchId, List<CommonPhoneModel> phoneList, AiSipCallPhone initCallPhone,
|
|
|
+ List<SopUserLogsInfo> userLogsInfos, Map<Long, String> phoneMap) {
|
|
|
+ final long startTime = System.currentTimeMillis();
|
|
|
+ log.info("========== 开始执行commonImportExcel方法, batchId={}, 原始数据量={} ==========", batchId, phoneList == null ? 0 : phoneList.size());
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 参数校验和任务验证
|
|
|
+ if (batchId == null || CollectionUtils.isEmpty(phoneList)) {
|
|
|
+ log.error("导入外呼任务失败: {}", batchId == null ? "batchId不能为空" : "导入数据不能为空");
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ AiSipCallTask task = validateTask(batchId);
|
|
|
+ if (task == null) return 0;
|
|
|
+
|
|
|
+ final Long remoteBatchId = task.getRemoteBatchId();
|
|
|
+ final int originalSize = phoneList.size();
|
|
|
+
|
|
|
+ // 2. 过滤无效和重复数据(号码维度)
|
|
|
+ List<CommonPhoneModel> validPhoneList = filterValidPhones(batchId, phoneList);
|
|
|
+ if (validPhoneList.isEmpty()) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2.1 同批次 fsUserId 去重(相对 DB 已有记录),防止重复外呼客户
|
|
|
+ Map<Long, String> workingPhoneMap = phoneMap == null ? new HashMap<>() : new HashMap<>(phoneMap);
|
|
|
+ List<CommonPhoneModel> uniquePhoneList = excludeExistingFsUserPhones(batchId, validPhoneList, workingPhoneMap);
|
|
|
+ if (uniquePhoneList.isEmpty()) {
|
|
|
+ log.info("commonImportExcel batchId={} 用户均已在批次中,跳过导入(防重复外呼)", batchId);
|
|
|
return 1;
|
|
|
- }else{
|
|
|
- log.error("停止外呼失败:{}", jsonObject.getString("msg"));
|
|
|
}
|
|
|
+ validPhoneList = uniquePhoneList;
|
|
|
+
|
|
|
+ // 3. 预处理用户信息映射(使用当前任务独立的 phoneMap,避免并发共享缓存互相干扰)
|
|
|
+ Map<String, SopUserLogsInfo> phoneUserInfoMap = buildPhoneUserInfoMap(userLogsInfos, workingPhoneMap);
|
|
|
+
|
|
|
+ // 4. 分批调用远程接口并保存
|
|
|
+ ImportResult result = importPhonesInBatches(batchId, remoteBatchId, validPhoneList,
|
|
|
+ (remoteList, batchIndex, totalBatches) -> {
|
|
|
+ // 填充字段回调
|
|
|
+ for (AiSipCallPhone callPhone : remoteList) {
|
|
|
+ fillCallPhoneFields(callPhone, initCallPhone, batchId, phoneUserInfoMap);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ final long duration = System.currentTimeMillis() - startTime;
|
|
|
+ log.info("========== commonImportExcel方法执行完成, batchId={}, 原始数据={}, 有效数据={}, 总处理数={}, 总成功数={}, 失败批次={}, 耗时={}ms ==========",
|
|
|
+ batchId, originalSize, validPhoneList.size(), result.totalProcessed, result.totalSuccessCount, result.failedBatchCount, duration);
|
|
|
+
|
|
|
+ return result.totalSuccessCount > 0 ? 1 : 0;
|
|
|
+ } catch (Exception e) {
|
|
|
+ final long duration = System.currentTimeMillis() - startTime;
|
|
|
+ log.error("========== commonImportExcel方法执行异常, batchId={}, 耗时={}ms ==========", batchId, duration, e);
|
|
|
+ return 0;
|
|
|
}
|
|
|
- return 0;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- public int commonImportExcel(Long batchId,List<CommonPhoneModel> phoneList) {
|
|
|
- if (batchId == null) {
|
|
|
- throw new RuntimeException("参数 batchId 不能为空");
|
|
|
+ public AjaxResult appUrgentClasImportData(Long batchId, Set<Long> fsUserIds, Long userId, String userName) {
|
|
|
+ final long startTime = System.currentTimeMillis();
|
|
|
+ log.info("========== 开始执行appUrgentClasImportData方法, batchId={}, 用户ID数量={} ==========",
|
|
|
+ batchId, fsUserIds == null ? 0 : fsUserIds.size());
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 参数校验
|
|
|
+ if (batchId == null || CollectionUtils.isEmpty(fsUserIds) || userId == null) {
|
|
|
+ return AjaxResult.error("参数不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1.1 屏蔽时段校验:命中则直接拒绝,不导入、不启动
|
|
|
+ if (aiSipAutoCallRulesService.isBatchIdInShieldTime(batchId)) {
|
|
|
+ log.info("appUrgentClasImportData batchId={} 处于屏蔽时段,拒绝导入", batchId);
|
|
|
+ return AjaxResult.error(SHIELD_TIME_FAIL_REASON);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 校验任务存在
|
|
|
+ AiSipCallTask task = validateTask(batchId);
|
|
|
+ if (task == null) {
|
|
|
+ return AjaxResult.error("任务不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ final Long remoteBatchId = task.getRemoteBatchId();
|
|
|
+
|
|
|
+ // 3. 批量获取用户手机号并构建外呼数据模型
|
|
|
+ List<CommonPhoneModel> phoneModels = buildPhoneModelsFromUsers(batchId, fsUserIds);
|
|
|
+ if (phoneModels == null || phoneModels.isEmpty()) {
|
|
|
+ return AjaxResult.error("无有效外呼数据");
|
|
|
+ }
|
|
|
+
|
|
|
+ final int originalSize = phoneModels.size();
|
|
|
+
|
|
|
+ // 4. 过滤无效和重复数据
|
|
|
+ List<CommonPhoneModel> validPhoneModels = filterValidPhones(batchId, phoneModels);
|
|
|
+ if (validPhoneModels.isEmpty()) {
|
|
|
+ return AjaxResult.error("导入外呼任务失败: 过滤后无有效数据, batchId=" + batchId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 构建手机号→fsUserId反向映射(提前构建,用于回调)
|
|
|
+ Map<Long, String> phoneMap = batchGetUserPhones(new ArrayList<>(fsUserIds));
|
|
|
+ Map<String, Long> phoneToFsUserIdMap = buildPhoneToUserIdMap(phoneMap);
|
|
|
+ final Long localBatchId = task.getBatchId();
|
|
|
+
|
|
|
+ // 6. 分批调用远程接口并保存
|
|
|
+ ImportResult result = importPhonesInBatches(batchId, remoteBatchId, validPhoneModels,
|
|
|
+ (remoteList, batchIndex, totalBatches) -> {
|
|
|
+ // 填充字段回调
|
|
|
+ fillAppUrgentFields(remoteList, localBatchId, userId, userName, phoneToFsUserIdMap);
|
|
|
+ });
|
|
|
+
|
|
|
+ if(result.totalSuccessCount <= 0){
|
|
|
+ return AjaxResult.error("导入外呼任务失败: batchId="+batchId+", 导入数量=0");
|
|
|
+ }
|
|
|
+ if (result.failedBatchCount > 0) {
|
|
|
+ log.error("存在失败批次: batchId={}, 失败批次数={}", batchId, result.failedBatchCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 启动外呼任务
|
|
|
+ int startTaskCount = this.startTask(batchId);
|
|
|
+ if (startTaskCount <= 0) {
|
|
|
+ log.error("成功导入但启动任务失败, batchId={}, 导入数量={}", batchId, result.totalSuccessCount);
|
|
|
+ return AjaxResult.error("成功导入但启动任务失败, batchId="+ batchId);
|
|
|
+ }
|
|
|
+
|
|
|
+ final long duration = System.currentTimeMillis() - startTime;
|
|
|
+ log.info("========== appUrgentClasImportData方法执行成功, batchId={}, 原始数据={}, 有效数据={}, 导入数量={}, 耗时={}ms ==========",
|
|
|
+ batchId, originalSize, validPhoneModels.size(), result.totalSuccessCount, duration);
|
|
|
+
|
|
|
+ return AjaxResult.success("成功导入并启动任务: batchId="+batchId+", 追加"+result.totalSuccessCount+"个名单");
|
|
|
+ } catch (Exception e) {
|
|
|
+ final long duration = System.currentTimeMillis() - startTime;
|
|
|
+ log.error("========== appUrgentClasImportData方法执行异常, batchId={}, 耗时={}ms ==========", batchId, duration, e);
|
|
|
+ return AjaxResult.error("batchId="+batchId+" 执行外呼导入和启动异常: "+ e.getMessage());
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
+ /**
|
|
|
+ * 验证任务存在性
|
|
|
+ */
|
|
|
+ private AiSipCallTask validateTask(Long batchId) {
|
|
|
AiSipCallTask task = this.selectAiSipCallTaskByBatchId(batchId);
|
|
|
if (task == null) {
|
|
|
- throw new RuntimeException("该任务不存在,请输入正确的 batchId");
|
|
|
+ log.error("导入外呼任务失败: 任务不存在, batchId={}", batchId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (task.getRemoteBatchId() == null) {
|
|
|
+ log.error("导入外呼任务失败: 远程任务ID为空, batchId={}", batchId);
|
|
|
+ return null;
|
|
|
}
|
|
|
+ return task;
|
|
|
+ }
|
|
|
|
|
|
- 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;
|
|
|
+ /**
|
|
|
+ * 过滤有效电话号码(去重 + 去除无效号码)
|
|
|
+ */
|
|
|
+ private List<CommonPhoneModel> filterValidPhones(Long batchId, List<CommonPhoneModel> phoneList) {
|
|
|
+ // 查询重复数据
|
|
|
+ List<String> duplicateList = aiSipCallPhoneService.isDuplicateEntry(batchId, phoneList);
|
|
|
+ Set<String> duplicateSet = CollectionUtils.isEmpty(duplicateList) ? Collections.emptySet() : new HashSet<>(duplicateList);
|
|
|
+
|
|
|
+ // 一次遍历完成过滤:排除库内重复 + 本批列表内号码去重
|
|
|
+ List<CommonPhoneModel> validList = new ArrayList<>(phoneList.size());
|
|
|
+ Set<String> seenInList = new HashSet<>(Math.max(16, phoneList.size() * 2));
|
|
|
+ for (CommonPhoneModel model : phoneList) {
|
|
|
+ if (model == null) continue;
|
|
|
+ String phoneNum = model.getPhoneNum();
|
|
|
+ if (StringUtils.isBlank(phoneNum) || "null".equalsIgnoreCase(phoneNum)) continue;
|
|
|
+ String normalized = phoneNum.trim();
|
|
|
+ if (!duplicateSet.isEmpty() && duplicateSet.contains(phoneNum)) continue;
|
|
|
+ if (!duplicateSet.isEmpty() && duplicateSet.contains(normalized)) continue;
|
|
|
+ if (!seenInList.add(normalized)) continue;
|
|
|
+ validList.add(model);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (validList.isEmpty()) {
|
|
|
+ String dupStr = duplicateList != null && !duplicateList.isEmpty()
|
|
|
+ ? String.join(",", duplicateList) : "无";
|
|
|
+ log.error("导入外呼任务失败: 过滤后无有效数据, batchId={}, 原始数量={}, 重复号码={}",
|
|
|
+ batchId, phoneList.size(), dupStr);
|
|
|
+ }
|
|
|
+
|
|
|
+ return validList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 剔除本批次 DB 中已存在 fsUserId 对应的号码,并同步收缩 workingPhoneMap。
|
|
|
+ */
|
|
|
+ private List<CommonPhoneModel> excludeExistingFsUserPhones(Long batchId,
|
|
|
+ List<CommonPhoneModel> phoneList,
|
|
|
+ Map<Long, String> workingPhoneMap) {
|
|
|
+ if (CollectionUtils.isEmpty(phoneList) || workingPhoneMap == null || workingPhoneMap.isEmpty()) {
|
|
|
+ return phoneList;
|
|
|
+ }
|
|
|
+ List<Long> existedList = aiSipCallPhoneService.selectDistinctFsUserIdsByLocalBatchId(batchId);
|
|
|
+ if (CollectionUtils.isEmpty(existedList)) {
|
|
|
+ return phoneList;
|
|
|
+ }
|
|
|
+ Set<Long> existed = new HashSet<>(existedList);
|
|
|
+ Set<String> removedPhones = new HashSet<>();
|
|
|
+ workingPhoneMap.entrySet().removeIf(entry -> {
|
|
|
+ if (entry.getKey() != null && existed.contains(entry.getKey())) {
|
|
|
+ if (StringUtils.isNotBlank(entry.getValue())) {
|
|
|
+ removedPhones.add(entry.getValue().trim());
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ });
|
|
|
+ if (removedPhones.isEmpty()) {
|
|
|
+ return phoneList;
|
|
|
+ }
|
|
|
+ List<CommonPhoneModel> kept = new ArrayList<>(phoneList.size());
|
|
|
+ for (CommonPhoneModel model : phoneList) {
|
|
|
+ if (model == null || StringUtils.isBlank(model.getPhoneNum())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (removedPhones.contains(model.getPhoneNum().trim())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ kept.add(model);
|
|
|
+ }
|
|
|
+ log.info("commonImportExcel batchId={} 按 fsUserId 剔除已存在号码 {} 个,剩余 {}",
|
|
|
+ batchId, phoneList.size() - kept.size(), kept.size());
|
|
|
+ return kept;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从用户ID集合构建外呼数据模型
|
|
|
+ */
|
|
|
+ private List<CommonPhoneModel> buildPhoneModelsFromUsers(Long batchId, Set<Long> fsUserIds) {
|
|
|
+ Map<Long, String> phoneMap = batchGetUserPhones(new ArrayList<>(fsUserIds));
|
|
|
+ if (phoneMap.isEmpty()) {
|
|
|
+ log.error("导入外呼任务失败: 未获取到有效手机号, batchId={}, 用户数={}", batchId, fsUserIds.size());
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ return buildPhoneModels(phoneMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建手机号到用户ID的映射
|
|
|
+ */
|
|
|
+ private Map<String, Long> buildPhoneToUserIdMap(Map<Long, String> phoneMap) {
|
|
|
+ Map<String, Long> phoneToUserIdMap = new HashMap<>(phoneMap.size());
|
|
|
+ for (Map.Entry<Long, String> entry : phoneMap.entrySet()) {
|
|
|
+ phoneToUserIdMap.putIfAbsent(entry.getValue(), entry.getKey());
|
|
|
+ }
|
|
|
+ return phoneToUserIdMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 填充appUrgentClasImportData特有字段
|
|
|
+ */
|
|
|
+ private void fillAppUrgentFields(List<AiSipCallPhone> remoteCallPhoneList, Long localBatchId,
|
|
|
+ Long userId, String userName, Map<String, Long> phoneToFsUserIdMap) {
|
|
|
+ final Long companyId = 323L;
|
|
|
+ final String executeType = "2";
|
|
|
+ final String callType = "1";
|
|
|
+
|
|
|
+ for (AiSipCallPhone callPhone : remoteCallPhoneList) {
|
|
|
+ callPhone.setLocalBatchId(localBatchId);
|
|
|
+ callPhone.setExecuteType(executeType);
|
|
|
+ callPhone.setCallType(callType);
|
|
|
+ callPhone.setCompanyId(companyId);
|
|
|
+ callPhone.setSysUserId(userId);
|
|
|
+ callPhone.setSysUserName(userName);
|
|
|
+
|
|
|
+ try {
|
|
|
+ String decryptPhone = SipDESUtil.decrypt(callPhone.getTelephone());
|
|
|
+ if (StringUtils.isNotBlank(decryptPhone)) {
|
|
|
+ Long fsUserId = phoneToFsUserIdMap.get(decryptPhone);
|
|
|
+ if (fsUserId != null) {
|
|
|
+ callPhone.setFsUserId(fsUserId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Throwable e) {
|
|
|
+ log.error("导入自动外呼记录id:{}时处理telephone解密失败:{}",callPhone.getId(),e.getMessage());
|
|
|
+ }
|
|
|
+ //处理电话
|
|
|
+ try {
|
|
|
+ if(StringUtils.isNotBlank(callPhone.getTelephone())){
|
|
|
+ callPhone.setTelephone(SipDESUtil.decrypt(URLDecoder.decode(callPhone.getTelephone(), "UTF-8").replaceAll("=+$", "")));
|
|
|
+ }else{
|
|
|
+ callPhone.setTelephone("");
|
|
|
+ }
|
|
|
+ if(StringUtils.isNotBlank(callPhone.getCallerNumber())){
|
|
|
+ callPhone.setCallerNumber(SipDESUtil.decrypt(URLDecoder.decode(callPhone.getCallerNumber(), "UTF-8").replaceAll("=+$", "")));
|
|
|
+ }else{
|
|
|
+ callPhone.setCallerNumber("");
|
|
|
+ }
|
|
|
+ } catch (Throwable e) {
|
|
|
+ callPhone.setTelephone("");
|
|
|
+ callPhone.setCallerNumber("");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @FunctionalInterface
|
|
|
+ private interface FieldFiller {
|
|
|
+ void fill(List<AiSipCallPhone> remoteList, int batchIndex, int totalBatches);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ImportResult {
|
|
|
+ int totalSuccessCount = 0;
|
|
|
+ int totalProcessed = 0;
|
|
|
+ int failedBatchCount = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ImportResult importPhonesInBatches(Long batchId, Long remoteBatchId,
|
|
|
+ List<CommonPhoneModel> validPhoneList, FieldFiller fieldFiller) {
|
|
|
+ // 优化批量大小:考虑到DES加密+Base64编码会使数据膨胀约33%
|
|
|
+ // 为保证HTTP请求体不超过500KB(安全阈值),每批500条最合适
|
|
|
+ // 计算:500条 * 360字节/条 * 1.33(加密膨胀) ≈ 240KB << 500KB安全线
|
|
|
+ final int batchSize = 500;
|
|
|
+ ImportResult result = new ImportResult();
|
|
|
+ final int totalBatches = (validPhoneList.size() + batchSize - 1) / batchSize;
|
|
|
+
|
|
|
+ log.info("开始批量导入外呼任务, batchId={}, 有效数据量={}, 总批次={}", batchId, validPhoneList.size(), totalBatches);
|
|
|
+
|
|
|
+ for (int i = 0; i < validPhoneList.size(); i += batchSize) {
|
|
|
+ final int batchIndex = i / batchSize + 1;
|
|
|
+ final int endIndex = Math.min(i + batchSize, validPhoneList.size());
|
|
|
+ final List<CommonPhoneModel> batchPhoneList = validPhoneList.subList(i, endIndex);
|
|
|
+
|
|
|
+ if (log.isInfoEnabled()) {
|
|
|
+ log.info("开始处理第{}/{}批数据, batchId={}, 当前批次大小={}", batchIndex, totalBatches, batchId, batchPhoneList.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用远程接口(带重试)
|
|
|
+ List<AiSipCallPhone> remoteList = callRemoteApiWithRetry(batchId, remoteBatchId, batchPhoneList, batchIndex, totalBatches);
|
|
|
+
|
|
|
+ if (remoteList != null && !remoteList.isEmpty()) {
|
|
|
+ // 填充字段
|
|
|
+ fieldFiller.fill(remoteList, batchIndex, totalBatches);
|
|
|
+
|
|
|
+ // 批量保存
|
|
|
+ int successCount = saveBatchPhones(remoteList);
|
|
|
+ result.totalSuccessCount += successCount;
|
|
|
+ result.totalProcessed += batchPhoneList.size();
|
|
|
+
|
|
|
+ if (log.isInfoEnabled()) {
|
|
|
+ log.info("第{}/{}批数据处理成功, batchId={}, 本批成功数={}, 累计成功数={}",
|
|
|
+ batchIndex, totalBatches, batchId, successCount, result.totalSuccessCount);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ result.failedBatchCount++;
|
|
|
+ log.error("批次[{}/{}]处理失败,该批次{}条数据未能导入, batchId={}",
|
|
|
+ batchIndex, totalBatches, batchPhoneList.size(), batchId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 数据完整性检查
|
|
|
+ if (result.totalProcessed != validPhoneList.size()) {
|
|
|
+ log.info("数据完整性警告: batchId={}, 预期处理={}, 实际处理={}, 丢失={}条",
|
|
|
+ batchId, validPhoneList.size(), result.totalProcessed, validPhoneList.size() - result.totalProcessed);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (result.failedBatchCount > 0) {
|
|
|
+ log.error("存在失败批次: batchId={}, 失败批次数={}, 总批次数={}",
|
|
|
+ batchId, result.failedBatchCount, totalBatches);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用远程接口(带重试机制)
|
|
|
+ * @return 成功返回解析后的列表,失败返回null
|
|
|
+ */
|
|
|
+ private List<AiSipCallPhone> callRemoteApiWithRetry(Long batchId, Long remoteBatchId,
|
|
|
+ List<CommonPhoneModel> phoneList,
|
|
|
+ int batchIndex, int totalBatches) {
|
|
|
+ int maxRetries = 3;
|
|
|
+
|
|
|
+ for (int retry = 0; retry < maxRetries; retry++) {
|
|
|
+ try {
|
|
|
+ CommonCallListModel callListModel = new CommonCallListModel();
|
|
|
+ callListModel.setBatchId(remoteBatchId);
|
|
|
+ callListModel.setPhoneList(phoneList);
|
|
|
+
|
|
|
+ String datajson = JSONObject.toJSONString(callListModel);
|
|
|
+ try {
|
|
|
+ datajson = SipDESUtil.encrypt(datajson);
|
|
|
+ } catch (Throwable e) {
|
|
|
+ log.error("batchId={}, 导入电话时数据加密失败", batchId, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String result = RemoteCommon.sendPost(RemoteCommon.REMOTE_ADDERSS_PREFIX + RemoteCommon.IMPORT_AUTOCALL_API,
|
|
|
+ URLEncoder.encode(datajson, "UTF-8"));
|
|
|
+
|
|
|
+ if (StringUtils.isBlank(result)) {
|
|
|
+ logRetryWarn(batchId, batchIndex, totalBatches, retry, maxRetries, "远程接口返回为空");
|
|
|
+ if (!doRetry(retry, maxRetries)) continue;
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject jsonObject = JSONObject.parseObject(result);
|
|
|
+ if (!RemoteApiHelper.isSuccess(jsonObject.getInteger("code"))) {
|
|
|
+ logRetryError(batchId, batchIndex, totalBatches, retry, maxRetries,
|
|
|
+ "远程接口返回失败: code=" + jsonObject.getInteger("code") + ", msg=" + jsonObject.getString("msg"));
|
|
|
+ if (!doRetry(retry, maxRetries)) continue;
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ String data = jsonObject.getString("data");
|
|
|
+ if (StringUtils.isBlank(data)) {
|
|
|
+ logRetryError(batchId, batchIndex, totalBatches, retry, maxRetries, "远程接口返回data为空");
|
|
|
+ if (!doRetry(retry, maxRetries)) continue;
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<AiSipCallPhone> remoteCallPhoneList = JSONObject.parseObject(data, new TypeReference<List<AiSipCallPhone>>(){});
|
|
|
+ if (CollectionUtils.isEmpty(remoteCallPhoneList)) {
|
|
|
+ logRetryError(batchId, batchIndex, totalBatches, retry, maxRetries, "转化返回数据为空");
|
|
|
+ if (!doRetry(retry, maxRetries)) continue;
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (batchIndex > 0) { // 只有分批时才记录成功日志
|
|
|
+ log.info("远程接口调用成功, batchId={}, 批次=[{}/{}], 返回数据量={}",
|
|
|
+ batchId, batchIndex, totalBatches, remoteCallPhoneList.size());
|
|
|
+ }
|
|
|
+ return remoteCallPhoneList;
|
|
|
+
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ log.error("远程接口调用被中断, batchId={}, 批次=[{}/{}]", batchId, batchIndex, totalBatches, e);
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ if (retry < maxRetries - 1) {
|
|
|
+ log.warn("远程接口调用异常, batchId={}, 批次=[{}/{}], 重试次数={}/{}, 错误: {}",
|
|
|
+ batchId, batchIndex, totalBatches, retry + 1, maxRetries, e.getMessage());
|
|
|
+ try {
|
|
|
+ Thread.sleep(1000 * (retry + 1));
|
|
|
+ } catch (InterruptedException ie) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.error("远程接口调用异常且重试耗尽, batchId={}, 批次=[{}/{}], 错误: {}",
|
|
|
+ batchId, batchIndex, totalBatches, e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断是否继续重试
|
|
|
+ */
|
|
|
+ private boolean doRetry(int retry, int maxRetries) throws InterruptedException {
|
|
|
+ if (retry < maxRetries - 1) {
|
|
|
+ Thread.sleep(1000 * (retry + 1));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 记录重试警告日志
|
|
|
+ */
|
|
|
+ private void logRetryWarn(Long batchId, int batchIndex, int totalBatches, int retry, int maxRetries, String message) {
|
|
|
+ if (batchIndex > 0) {
|
|
|
+ log.warn("{}, batchId={}, 批次=[{}/{}], 重试次数={}/{}", message, batchId, batchIndex, totalBatches, retry + 1, maxRetries);
|
|
|
+ } else {
|
|
|
+ log.warn("{}, batchId={}, 重试次数={}/{}", message, batchId, retry + 1, maxRetries);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 记录重试错误日志
|
|
|
+ */
|
|
|
+ private void logRetryError(Long batchId, int batchIndex, int totalBatches, int retry, int maxRetries, String message) {
|
|
|
+ if (log.isErrorEnabled()) {
|
|
|
+ if (batchIndex > 0) {
|
|
|
+ log.error("{}, batchId={}, 批次=[{}/{}], 重试次数={}/{}", message, batchId, batchIndex, totalBatches, retry + 1, maxRetries);
|
|
|
+ } else {
|
|
|
+ log.error("{}, batchId={}, 重试次数={}/{}", message, batchId, retry + 1, maxRetries);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建手机号到用户信息的映射(基于当前任务独立的 userId->phone 映射,保证并发安全)
|
|
|
+ */
|
|
|
+ private Map<String, SopUserLogsInfo> buildPhoneUserInfoMap(List<SopUserLogsInfo> userLogsInfos,
|
|
|
+ Map<Long, String> phoneMap) {
|
|
|
+ Map<String, SopUserLogsInfo> phoneUserInfoMap = new HashMap<>();
|
|
|
+
|
|
|
+ if (CollectionUtils.isEmpty(userLogsInfos) || phoneMap == null || phoneMap.isEmpty()) {
|
|
|
+ return phoneUserInfoMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<Long, SopUserLogsInfo> userIdToInfoMap = userLogsInfos.stream()
|
|
|
+ .filter(info -> info != null && info.getFsUserId() != null)
|
|
|
+ .collect(Collectors.toMap(SopUserLogsInfo::getFsUserId, info -> info, (k1, k2) -> k1));
|
|
|
+
|
|
|
+ for (Map.Entry<Long, String> entry : phoneMap.entrySet()) {
|
|
|
+ Long userId = entry.getKey();
|
|
|
+ String phone = entry.getValue();
|
|
|
+ if (userId == null || StringUtils.isBlank(phone)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ SopUserLogsInfo userInfo = userIdToInfoMap.get(userId);
|
|
|
+ if (userInfo != null) {
|
|
|
+ phoneUserInfoMap.putIfAbsent(phone.trim(), userInfo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return phoneUserInfoMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 填充通话记录字段
|
|
|
+ */
|
|
|
+ private void fillCallPhoneFields(AiSipCallPhone callPhone, AiSipCallPhone initCallPhone, Long batchId, Map<String, SopUserLogsInfo> phoneUserInfoMap) {
|
|
|
+ // 复制基础字段
|
|
|
+ callPhone.setCallType(initCallPhone.getCallType());
|
|
|
+ callPhone.setCompanyId(initCallPhone.getCompanyId());
|
|
|
+ callPhone.setCompanyUserId(initCallPhone.getCompanyUserId());
|
|
|
+ callPhone.setCompanyUserName(initCallPhone.getCompanyUserName());
|
|
|
+ callPhone.setSysUserId(initCallPhone.getSysUserId());
|
|
|
+ callPhone.setSysUserName(initCallPhone.getSysUserName());
|
|
|
+ callPhone.setLocalBatchId(batchId);
|
|
|
+ callPhone.setQwUserid(initCallPhone.getQwUserid());
|
|
|
+ callPhone.setSendTime(initCallPhone.getSendTime());
|
|
|
+ callPhone.setSopId(initCallPhone.getSopId());
|
|
|
+ callPhone.setCorpId(initCallPhone.getCorpId());
|
|
|
+ callPhone.setSendType(initCallPhone.getSendType());
|
|
|
+ callPhone.setCourseId(initCallPhone.getCourseId());
|
|
|
+ callPhone.setVideoId(initCallPhone.getVideoId());
|
|
|
+ callPhone.setCourseType(initCallPhone.getCourseType());
|
|
|
+ callPhone.setExecuteType(initCallPhone.getExecuteType());
|
|
|
+ callPhone.setSipTaskId(initCallPhone.getSipTaskId());
|
|
|
+ callPhone.setAppCustomerId(initCallPhone.getAppCustomerId());
|
|
|
+
|
|
|
+ // 设置用户关联信息
|
|
|
+ if (StringUtils.isNotBlank(callPhone.getTelephone()) && !phoneUserInfoMap.isEmpty()) {
|
|
|
+ try {
|
|
|
+ String decryptPhone = SipDESUtil.decrypt(callPhone.getTelephone());
|
|
|
+ if (StringUtils.isNotBlank(decryptPhone)) {
|
|
|
+ SopUserLogsInfo userInfo = phoneUserInfoMap.get(decryptPhone.trim());
|
|
|
+ if (userInfo != null) {
|
|
|
+ callPhone.setFsUserId(userInfo.getFsUserId());
|
|
|
+ if (userInfo.getExternalId() != null) {
|
|
|
+ callPhone.setExternalId(String.valueOf(userInfo.getExternalId()));
|
|
|
+ callPhone.setExternalUserId(String.valueOf(userInfo.getExternalId()));
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(userInfo.getExternalUserName())) {
|
|
|
+ callPhone.setExternalUserName(userInfo.getExternalUserName());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Throwable e) {
|
|
|
+ log.error("导入自动外呼记录id:{}时处理telephone解密失败:{}",callPhone.getId(),e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //处理电话
|
|
|
+ try{
|
|
|
+ if(StringUtils.isNotEmpty(callPhone.getTelephone())){
|
|
|
+ callPhone.setTelephone(URLEncoder.encode(callPhone.getTelephone(), "UTF-8"));
|
|
|
}else{
|
|
|
- log.error("任务:{}添加名单失败:{}",batchId, jsonObject.getString("msg"));
|
|
|
+ callPhone.setTelephone("");
|
|
|
}
|
|
|
+ }catch (Throwable e){
|
|
|
+ log.error("导入自动外呼记录id:{}时处理telephone编码失败:{}",callPhone.getId(),e.getMessage());
|
|
|
+ callPhone.setTelephone("");
|
|
|
+ }
|
|
|
+ try{
|
|
|
+ if(StringUtils.isNotEmpty(callPhone.getCallerNumber())){
|
|
|
+ callPhone.setCallerNumber(URLEncoder.encode(callPhone.getCallerNumber(), "UTF-8"));
|
|
|
+ }else{
|
|
|
+ callPhone.setCallerNumber("");
|
|
|
+ }
|
|
|
+ }catch (Throwable e){
|
|
|
+ log.error("导入自动外呼记录id:{}时处理callerNumber编码失败:{}",callPhone.getId(),e.getMessage());
|
|
|
+ callPhone.setCallerNumber("");
|
|
|
}
|
|
|
- return 0;
|
|
|
}
|
|
|
|
|
|
- @Override
|
|
|
- public CallTaskStatModel statByBatchId(Long batchId) {
|
|
|
- return aiSipCallPhoneService.statByBatchId(batchId);
|
|
|
+ /**
|
|
|
+ * 批量保存通话记录(每200条提交一次,带异常处理和重试)
|
|
|
+ */
|
|
|
+ private int saveBatchPhones(List<AiSipCallPhone> phoneList) {
|
|
|
+ if (CollectionUtils.isEmpty(phoneList)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ final int batchSize = 200;
|
|
|
+ int totalSaved = 0;
|
|
|
+ int failedBatchCount = 0;
|
|
|
+ List<AiSipCallPhone> batchList = new ArrayList<>(batchSize);
|
|
|
+
|
|
|
+ for (AiSipCallPhone phone : phoneList) {
|
|
|
+ batchList.add(phone);
|
|
|
+ if (batchList.size() >= batchSize) {
|
|
|
+ // 带重试的批量保存
|
|
|
+ boolean saved = saveBatchWithRetry(batchList, 3);
|
|
|
+ if (saved) {
|
|
|
+ totalSaved += batchList.size();
|
|
|
+ } else {
|
|
|
+ failedBatchCount++;
|
|
|
+ log.error("批量保存失败,该批次{}条数据未能保存", batchList.size());
|
|
|
+ }
|
|
|
+ batchList.clear(); // 复用列表,避免重复创建
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存剩余数据
|
|
|
+ if (!batchList.isEmpty()) {
|
|
|
+ boolean saved = saveBatchWithRetry(batchList, 3);
|
|
|
+ if (saved) {
|
|
|
+ totalSaved += batchList.size();
|
|
|
+ } else {
|
|
|
+ failedBatchCount++;
|
|
|
+ log.error("批量保存失败,最后批次{}条数据未能保存", batchList.size());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (failedBatchCount > 0) {
|
|
|
+ log.error("批量保存存在失败批次: 总批次={}, 失败批次={}, 成功保存={}",
|
|
|
+ (phoneList.size() + batchSize - 1) / batchSize, failedBatchCount, totalSaved);
|
|
|
+ }
|
|
|
+
|
|
|
+ return totalSaved;
|
|
|
}
|
|
|
|
|
|
- @Override
|
|
|
- public AiSipCallTask selectAiSipCallTaskByRemoteBatchId(Long remoteBatchId) {
|
|
|
- return baseMapper.selectAiSipCallTaskByRemoteBatchId(remoteBatchId);
|
|
|
+ /**
|
|
|
+ * 带重试机制的批量保存
|
|
|
+ * @param batchList 待保存的数据列表
|
|
|
+ * @param maxRetries 最大重试次数
|
|
|
+ * @return 是否保存成功
|
|
|
+ */
|
|
|
+ private boolean saveBatchWithRetry(List<AiSipCallPhone> batchList, int maxRetries) {
|
|
|
+ for (int retry = 0; retry < maxRetries; retry++) {
|
|
|
+ try {
|
|
|
+ aiSipCallPhoneService.saveBatch(batchList);
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ if (retry < maxRetries - 1) {
|
|
|
+ log.warn("批量保存失败,重试次数={}/{}, 错误: {}", retry + 1, maxRetries, e.getMessage());
|
|
|
+ try {
|
|
|
+ Thread.sleep(500 * (retry + 1)); // 递增延迟:500ms, 1s, 1.5s
|
|
|
+ } catch (InterruptedException ie) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ log.error("重试等待时被中断", ie);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.error("批量保存失败且重试耗尽, 批次大小={}, 错误: {}", batchList.size(), e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 手机号缓存(长期缓存,跨任务周期共享)
|
|
|
+ private final Cache<Long, String> phoneCache = CacheBuilder.newBuilder()
|
|
|
+ .expireAfterWrite(1, TimeUnit.HOURS)
|
|
|
+ .maximumSize(100000)
|
|
|
+ .build();
|
|
|
+ /**
|
|
|
+ * 批量获取用户手机号(高性能版本)
|
|
|
+ */
|
|
|
+ private Map<Long, String> batchGetUserPhones(List<Long> userIds) {
|
|
|
+ if (userIds == null || userIds.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 预分配容量,避免扩容
|
|
|
+ Map<Long, String> phoneMap = new HashMap<>(userIds.size());
|
|
|
+ Set<Long> missingUserIds = new HashSet<>();
|
|
|
+
|
|
|
+ // 先从缓存获取
|
|
|
+ for (Long userId : userIds) {
|
|
|
+ if (userId == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String phone = phoneCache.getIfPresent(userId);
|
|
|
+ if (phone != null) {
|
|
|
+ phoneMap.put(userId, phone);
|
|
|
+ } else {
|
|
|
+ missingUserIds.add(userId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量查询缺失的手机号
|
|
|
+ if (!missingUserIds.isEmpty()) {
|
|
|
+ int batchSize = 300;
|
|
|
+ List<Long> missingUserIdList = new ArrayList<>(missingUserIds);
|
|
|
+
|
|
|
+ for (int i = 0; i < missingUserIdList.size(); i += batchSize) {
|
|
|
+ int endIndex = Math.min(i + batchSize, missingUserIdList.size());
|
|
|
+ List<Long> batchUserIds = missingUserIdList.subList(i, endIndex);
|
|
|
+
|
|
|
+ try {
|
|
|
+ List<FsUser> userList = fsUserService.selectUserListByUserIds(batchUserIds);
|
|
|
+ if (userList != null && !userList.isEmpty()) {
|
|
|
+ for (FsUser user : userList) {
|
|
|
+ if (user != null && user.getUserId() != null && user.getPhone() != null) {
|
|
|
+ try {
|
|
|
+ //测试数据
|
|
|
+// String phone ="15779601354";
|
|
|
+ String phone = PhoneUtil.decryptPhone(user.getPhone());
|
|
|
+ if (phone != null && !phone.trim().isEmpty()) {
|
|
|
+ phoneMap.put(user.getUserId(), phone);
|
|
|
+ phoneCache.put(user.getUserId(), phone);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解密用户 {} 手机号失败: {}", user.getUserId(), e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("批量查询用户手机号失败,批次大小: {}", batchUserIds.size(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return phoneMap;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 构建外呼数据模型(脱敏处理,避免敏感信息泄露)
|
|
|
+ */
|
|
|
+ private List<CommonPhoneModel> buildPhoneModels(Map<Long, String> phoneMap) {
|
|
|
+ if (phoneMap == null || phoneMap.isEmpty()) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<CommonPhoneModel> phoneList = new ArrayList<>(phoneMap.size());
|
|
|
+
|
|
|
+ // 从 phoneMap 中提取所有的手机号(value)
|
|
|
+ for (String phone : phoneMap.values()) {
|
|
|
+ if (phone != null && !phone.trim().isEmpty()) {
|
|
|
+ CommonPhoneModel model = new CommonPhoneModel();
|
|
|
+ model.setPhoneNum(phone);
|
|
|
+ model.setNoticeContent("runtian"); // 提醒内容
|
|
|
+
|
|
|
+ JSONObject bizJson = new JSONObject();
|
|
|
+ bizJson.put("custName", "runtian");
|
|
|
+ model.setBizJson(bizJson);
|
|
|
+
|
|
|
+ phoneList.add(model);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return phoneList;
|
|
|
}
|
|
|
}
|