Browse Source

cid新增手动同步外呼结果触发工作流

lmx 17 hours ago
parent
commit
c48097f467

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

@@ -9,6 +9,7 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.service.ICompanyAiCallDataSyncService;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerService;
@@ -17,6 +18,7 @@ import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.framework.service.TokenService;
+import com.fs.wxcid.utils.TenantHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
@@ -41,6 +43,26 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
     private ICrmCustomerService crmCustomerService;
     private ICrmCustomerService crmCustomerService;
     @Autowired
     @Autowired
     private TokenService tokenService;
     private TokenService tokenService;
+
+    @Autowired
+    private ICompanyAiCallDataSyncService companyAiCallDataSyncService;
+
+    /**
+     * 异步同步 AI 外呼数据(补偿 EasyCall 已完成但未回调的记录)
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphoneDetail:list')")
+    @Log(title = "同步AI外呼数据", businessType = BusinessType.OTHER)
+    @PostMapping("/syncAiCallData")
+    public AjaxResult syncAiCallData() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        TenantHelper.setTenantId(loginUser.getTenantId());
+        if (!companyAiCallDataSyncService.tryStartSync(companyId)) {
+            return AjaxResult.error("正在同步中,请勿重复操作");
+        }
+        return AjaxResult.success("正在同步中");
+    }
+
     /**
     /**
      * 查询调用日志_ai打电话列表
      * 查询调用日志_ai打电话列表
      */
      */

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

@@ -115,4 +115,9 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      * 按 customer_id(= callees.user_id)批量统计累计 AI 外呼总数与接通数
      * 按 customer_id(= callees.user_id)批量统计累计 AI 外呼总数与接通数
      */
      */
     List<CustomerCallStatVO> selectAiCallStatTotal(@Param("customerIds") List<Long> customerIds);
     List<CustomerCallStatVO> selectAiCallStatTotal(@Param("customerIds") List<Long> customerIds);
+
+    /**
+     * 查询执行中任务下、外呼记录仍为执行中的 callbackUuid 列表
+     */
+    List<String> selectRunningCallbackUuidsByCompanyId(@Param("companyId") Long companyId);
 }
 }

+ 8 - 0
fs-service/src/main/java/com/fs/company/mapper/EasyCallMapper.java

@@ -8,6 +8,8 @@ import com.fs.company.vo.easycall.EasyCallOutBoundVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 import org.springframework.stereotype.Repository;
 import org.springframework.stereotype.Repository;
 
 
+import java.util.List;
+
 /**
 /**
  * @author MixLiu
  * @author MixLiu
  * @date 2026/3/7 10:43
  * @date 2026/3/7 10:43
@@ -27,4 +29,10 @@ public interface EasyCallMapper {
     @DataSource(DataSourceType.EASYCALL)
     @DataSource(DataSourceType.EASYCALL)
     InboundCallInfo selectInboundCallbackInfoByUuid(@Param("uuid") String uuid);
     InboundCallInfo selectInboundCallbackInfoByUuid(@Param("uuid") String uuid);
 
 
+    /**
+     * 根据 callbackUuid 批量查询 EasyCall 已外呼完成的话单 uuid
+     */
+    @DataSource(DataSourceType.EASYCALL)
+    List<String> selectCompletedUuidsByCallbackUuids(@Param("callbackUuids") List<String> callbackUuids);
+
 }
 }

+ 15 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyAiCallDataSyncService.java

@@ -0,0 +1,15 @@
+package com.fs.company.service;
+
+/**
+ * AI外呼数据同步服务
+ */
+public interface ICompanyAiCallDataSyncService {
+
+    /**
+     * 尝试启动异步同步任务(同公司防重复)
+     *
+     * @param companyId 公司ID
+     * @return true-已启动;false-已有任务在运行
+     */
+    boolean tryStartSync(Long companyId);
+}

+ 91 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyAiCallDataSyncServiceImpl.java

@@ -0,0 +1,91 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.core.redis.RedisCache;
+import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
+import com.fs.company.mapper.EasyCallMapper;
+import com.fs.company.service.ICompanyAiCallDataSyncService;
+import com.fs.company.service.ICompanyVoiceRoboticService;
+import com.fs.company.vo.CdrDetailVo;
+import com.fs.wxcid.utils.TenantHelper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * AI外呼数据同步:补偿 EasyCall 已外呼完成但未回调入库的记录
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CompanyAiCallDataSyncServiceImpl implements ICompanyAiCallDataSyncService {
+
+    private static final String SYNC_LOCK_PREFIX = "ai_call_sync:company:";
+    private static final int BATCH_SIZE = 1000;
+    private static final long LOCK_TIMEOUT_HOURS = 2L;
+
+    private final RedisCache redisCache;
+    private final CompanyVoiceRoboticCallLogCallphoneMapper callLogCallphoneMapper;
+    private final EasyCallMapper easyCallMapper;
+    private final ICompanyVoiceRoboticService companyVoiceRoboticService;
+
+
+    @Override
+    public boolean tryStartSync(Long companyId) {
+        String lockKey = buildLockKey(companyId);
+        if (!redisCache.setIfAbsent(lockKey, "1", LOCK_TIMEOUT_HOURS, TimeUnit.HOURS)) {
+            return false;
+        }
+        doSyncAiCallDataAsync(companyId, lockKey);
+        return true;
+    }
+
+    @Async("callLogExcutor")
+    public void doSyncAiCallDataAsync(Long companyId, String lockKey) {
+        try {
+            List<String> callbackUuids = callLogCallphoneMapper.selectRunningCallbackUuidsByCompanyId(companyId);
+            if (callbackUuids == null || callbackUuids.isEmpty()) {
+                log.info("syncAiCallData: 无待补偿外呼记录, companyId={}", companyId);
+                return;
+            }
+            log.info("syncAiCallData: 开始同步, companyId={}, 待检查callbackUuid数={}", companyId, callbackUuids.size());
+            int triggered = 0;
+            for (int i = 0; i < callbackUuids.size(); i += BATCH_SIZE) {
+                List<String> batch = callbackUuids.subList(i, Math.min(i + BATCH_SIZE, callbackUuids.size()));
+                List<String> completedUuids = easyCallMapper.selectCompletedUuidsByCallbackUuids(batch);
+                if (completedUuids == null || completedUuids.isEmpty()) {
+                    continue;
+                }
+                for (String uuid : completedUuids) {
+                    try {
+                        CdrDetailVo cdrDetailVo = new CdrDetailVo();
+                        cdrDetailVo.setUuid(uuid);
+                        cdrDetailVo.setCdrType("outbound");
+                        companyVoiceRoboticService.callerResult4EasyCall(cdrDetailVo);
+                        triggered++;
+                    } catch (Exception e) {
+                        log.error("syncAiCallData: 触发回调失败, uuid={}, companyId={}", uuid, companyId, e);
+                    }
+                }
+            }
+            log.info("syncAiCallData: 同步完成, companyId={}, 触发回调数={}", companyId, triggered);
+        } catch (Exception e) {
+            log.error("syncAiCallData: 同步异常, companyId={}", companyId, e);
+        } finally {
+            redisCache.deleteObject(lockKey);
+        }
+    }
+
+    private String buildLockKey(Long companyId) {
+        Long tenantId = TenantHelper.getTenantId();
+        if (tenantId != null) {
+            return SYNC_LOCK_PREFIX + tenantId + ":" + companyId;
+        }
+        return SYNC_LOCK_PREFIX + companyId;
+    }
+}

+ 1 - 2
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -298,8 +298,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         callbackInfo.put("nodeKey", context.getCurrentNodeKey());
         callbackInfo.put("nodeKey", context.getCurrentNodeKey());
         callbackInfo.put("workflowInstanceId", context.getWorkflowInstanceId());
         callbackInfo.put("workflowInstanceId", context.getWorkflowInstanceId());
         callbackInfo.put("calleeId", calleeId);
         callbackInfo.put("calleeId", calleeId);
-        super.redisCache.setCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + callBackUuid,
-                callbackInfo.toJSONString(), 15, TimeUnit.DAYS);
+        super.redisCache.setCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + callBackUuid,callbackInfo.toJSONString());
         // 将 callBackUuid 写入 context,供后续回调时从 context 取用
         // 将 callBackUuid 写入 context,供后续回调时从 context 取用
         context.setVariable("callBackUuid", callBackUuid);
         context.setVariable("callBackUuid", callBackUuid);
 
 

+ 12 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml

@@ -200,6 +200,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         select * from company_voice_robotic_call_log_callphone where callback_uuid = #{uuid}
         select * from company_voice_robotic_call_log_callphone where callback_uuid = #{uuid}
     </select>
     </select>
 
 
+    <select id="selectRunningCallbackUuidsByCompanyId" resultType="java.lang.String">
+        SELECT DISTINCT t1.callback_uuid
+        FROM company_voice_robotic_call_log_callphone t1
+        INNER JOIN company_voice_robotic cvr ON cvr.id = t1.robotic_id
+        WHERE cvr.company_id = #{companyId}
+          AND cvr.task_status = 1
+          AND (cvr.del_flag = 0 OR cvr.del_flag IS NULL)
+          AND t1.status = 1
+          AND t1.callback_uuid IS NOT NULL
+          AND t1.callback_uuid != ''
+    </select>
+
     <select id="countTodayCallsByBusinessId" resultType="int">
     <select id="countTodayCallsByBusinessId" resultType="int">
         SELECT IFNULL(COUNT(*), 0)
         SELECT IFNULL(COUNT(*), 0)
         FROM company_voice_robotic_callees es
         FROM company_voice_robotic_callees es

+ 10 - 0
fs-service/src/main/resources/mapper/company/EasyCallMapper.xml

@@ -24,4 +24,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             limit 1
             limit 1
     </select>
     </select>
 
 
+    <select id="selectCompletedUuidsByCallbackUuids" resultType="java.lang.String">
+        SELECT uuid
+        FROM cc_call_phone
+        WHERE callout_time != 0
+        AND JSON_UNQUOTE(JSON_EXTRACT(biz_json, '$.callBackUuid')) IN
+        <foreach collection="callbackUuids" item="item" open="(" separator="," close=")">
+            #{item}
+        </foreach>
+    </select>
+
 </mapper>
 </mapper>