/**
|
* 案件数据全局Context
|
* 提供案件数据的全局状态管理和localStorage持久化
|
*/
|
|
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
import { message } from 'antd';
|
import ProcessAPIService from '../services/ProcessAPIService';
|
import OutboundBotAPIService from '../services/OutboundBotAPIService';
|
import { getMergedParams } from '../utils/urlParams';
|
import { mockTimelineData } from '../mocks/timeline';
|
import { getFallbackStartTime, parseTimeString } from '../utils/timeFormatter';
|
|
// 创建Context
|
const CaseDataContext = createContext(null);
|
|
// localStorage键名
|
const STORAGE_KEY = 'case_data_timeline';
|
const OUTBOUND_JOBS_KEY = 'outbound_call_jobs';
|
|
/**
|
* CaseDataProvider组件
|
* 包裹需要访问案件数据的组件树
|
*/
|
export const CaseDataProvider = ({ children }) => {
|
const [caseData, setCaseData] = useState(null);
|
const [processNodes, setProcessNodes] = useState([]); // 流程节点数据
|
const [loading, setLoading] = useState(false);
|
const [error, setError] = useState(null);
|
const [taskStartTime, setTaskStartTime] = useState(null); // 任务开始时间
|
const [isTaskTimeFallback, setIsTaskTimeFallback] = useState(false); // 是否降级模式
|
const [hasLoaded, setHasLoaded] = useState(false); // 防止重复加载
|
|
/**
|
* 保存数据到localStorage
|
*/
|
const saveToStorage = (data) => {
|
try {
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
} catch (err) {
|
console.error('Failed to save data to localStorage:', err);
|
}
|
};
|
|
/**
|
* 检查是否有活跃的外呼任务
|
* @returns {boolean} 是否有活跃任务
|
*/
|
const hasActiveOutboundJobs = () => {
|
try {
|
const stored = localStorage.getItem(OUTBOUND_JOBS_KEY);
|
if (!stored) return false;
|
|
const jobs = JSON.parse(stored);
|
const activeStatuses = [
|
'Scheduling',
|
'Executing',
|
'Paused',
|
'Drafted',
|
'InProgress',
|
'Calling',
|
'Ringing',
|
'Answered'
|
];
|
|
// 过滤出活跃状态的任务
|
const activeJobs = jobs.filter(job => activeStatuses.includes(job.callStatus));
|
return activeJobs.length > 0;
|
} catch (err) {
|
console.error('检查活跃外呼任务失败:', err);
|
return false;
|
}
|
};
|
|
/**
|
* 触发智能外呼
|
* @param {Object} timeline - 案件时间线数据
|
*/
|
const triggerOutboundCall = async (timeline) => {
|
try {
|
const { mediation } = timeline;
|
if (!mediation) {
|
console.warn('缺少必要参数:timeline.mediation,跳过外呼触发:', timeline);
|
return;
|
}
|
const { state } = mediation;
|
if (state >= 2) { // 2:调解成功,3:调解失败,4:人工接管
|
console.warn('调解状态已结束,mediation.state:', state);
|
return;
|
}
|
// 检查是否已有活跃任务,如有则跳过
|
if (hasActiveOutboundJobs()) {
|
console.log('检测到活跃外呼任务,跳过发起新外呼');
|
return;
|
}
|
|
const mediationId = timeline.id;
|
const caseId = timeline.case_id;
|
|
if (!mediationId || !caseId) {
|
console.warn('缺少必要参数,跳过外呼触发:', { mediationId, caseId });
|
return;
|
}
|
|
console.log('发起智能外呼,mediationId:', mediationId, ', caseId:', caseId);
|
|
// 调用外呼API
|
const response = await OutboundBotAPIService.makeCallV2({
|
mediationId: String(mediationId),
|
caseId: String(caseId),
|
callAuto: 0,
|
callPersonId: ''
|
});
|
|
// 处理响应
|
console.log('triggerOutboundCall API 响应:', response);
|
console.log('response.data:', response?.data);
|
console.log('response.data 是数组:', Array.isArray(response?.data));
|
|
if (response?.data && Array.isArray(response.data)) {
|
const successJobs = [];
|
const failedJobs = [];
|
|
// 分类成功和失败的记录
|
response.data.forEach(item => {
|
if (item.errorCode === 0 && item.jobId) {
|
successJobs.push({
|
jobId: item.jobId,
|
callStatus: item.callStatus || 'Scheduling',
|
personId: item.personId,
|
mediationId: item.mediationId,
|
caseId: String(caseId), // 添加 caseId 字段用于轮询
|
perClassName: item.perClassName || '', // 添加人员类型名称
|
trueName: item.trueName || '', // 添加真实姓名
|
startTime: item.createTime || item.createdTime || item.start_time,
|
pollStartTime: Date.now(),
|
retryCount: 0
|
});
|
} else if (item.errorCode > 0) {
|
failedJobs.push({
|
personId: item.personId,
|
message: item.message || '未知错误',
|
perClassName: item.perClassName || '', // 添加人员类型名称
|
trueName: item.trueName || '', // 添加真实姓名
|
errorCode: item.errorCode, // 添加错误码
|
startTime: new Date(item.startTime || item.start_time).getTime() // 转换为时间戳
|
});
|
}
|
});
|
|
// 存储成功的任务到 localStorage
|
if (successJobs.length > 0) {
|
// 读取现有的成功任务,合并而不是覆盖
|
const existingJobs = JSON.parse(localStorage.getItem(OUTBOUND_JOBS_KEY) || '[]');
|
// 按 jobId 去重,新任务优先
|
const existingJobIds = new Set(successJobs.map(job => job.jobId));
|
const filteredExistingJobs = existingJobs.filter(job => !existingJobIds.has(job.jobId));
|
const mergedJobs = [...successJobs, ...filteredExistingJobs];
|
|
localStorage.setItem(OUTBOUND_JOBS_KEY, JSON.stringify(mergedJobs));
|
console.log('存储外呼任务成功,新增:', successJobs.length, '总数:', mergedJobs.length);
|
console.log('存储的任务:', JSON.stringify(mergedJobs, null, 2));
|
|
// 外呼成功后,清除对应的失败记录
|
const storedFailedJobs = JSON.parse(localStorage.getItem(`${OUTBOUND_JOBS_KEY}_failed`) || '[]');
|
if (storedFailedJobs.length > 0) {
|
// 获取成功任务的 personId 列表
|
const successPersonIds = successJobs.map(job => job.personId);
|
// 过滤掉已成功的 personId 对应的失败记录
|
const remainingFailedJobs = storedFailedJobs.filter(job => !successPersonIds.includes(job.personId));
|
localStorage.setItem(`${OUTBOUND_JOBS_KEY}_failed`, JSON.stringify(remainingFailedJobs));
|
console.log('外呼成功后清除失败记录,清除数量:', storedFailedJobs.length - remainingFailedJobs.length);
|
}
|
|
// 延迟触发自定义事件,确保监听器已经设置好
|
setTimeout(() => {
|
console.log('触发 outbound-jobs-updated 事件');
|
window.dispatchEvent(new CustomEvent('outbound-jobs-updated'));
|
}, 300);
|
}
|
|
// 存储失败的任务到 localStorage(用于气泡显示)
|
if (failedJobs.length > 0) {
|
console.log('准备存储失败任务:', failedJobs);
|
// 读取现有的失败任务
|
const storedFailedJobs = JSON.parse(localStorage.getItem(`${OUTBOUND_JOBS_KEY}_failed`) || '[]');
|
|
// 去重:按 personId 去重,保留最新的错误信息
|
const uniqueFailedJobs = [...storedFailedJobs];
|
failedJobs.forEach(newJob => {
|
const existingIndex = uniqueFailedJobs.findIndex(job => job.personId === newJob.personId);
|
if (existingIndex >= 0) {
|
// 更新已存在的失败任务
|
uniqueFailedJobs[existingIndex] = newJob;
|
} else {
|
// 添加新的失败任务
|
uniqueFailedJobs.push(newJob);
|
}
|
});
|
|
// 清理超过24小时的失败任务
|
const now = Date.now();
|
const cleanedFailedJobs = uniqueFailedJobs.filter(job => {
|
return (now - job.startTime) < 24 * 60 * 60 * 1000; // 24小时
|
});
|
|
localStorage.setItem(`${OUTBOUND_JOBS_KEY}_failed`, JSON.stringify(cleanedFailedJobs));
|
console.log('存储外呼失败任务,数量:', cleanedFailedJobs.length);
|
|
// 触发自定义事件通知 OutboundCallWidget 组件刷新
|
window.dispatchEvent(new CustomEvent('outbound-jobs-updated'));
|
}
|
|
// 提示失败的任务
|
if (failedJobs.length > 0) {
|
const failedMsg = failedJobs
|
.map(job => `${job.personId}(${job.message})`)
|
.join('、');
|
message.warning(`部分联系人外呼失败:${failedMsg}`);
|
console.warn('部分外呼失败:', failedJobs);
|
}
|
}
|
} catch (err) {
|
console.error('智能外呼发起失败:', err);
|
message.error('智能外呼发起失败,请检查网络后重试');
|
}
|
};
|
|
/**
|
* 加载任务时间数据
|
*/
|
const loadTaskTime = async (timeline) => {
|
try {
|
const mediationId = timeline.id;
|
const nodeId = timeline.current_node?.id;
|
|
if (!mediationId || !nodeId) {
|
throw new Error('缺少必要的参数: mediation_id 或 node_id');
|
}
|
|
console.log('Loading task time with:', { mediationId, nodeId });
|
|
const response = await ProcessAPIService.getTaskTime(mediationId, nodeId);
|
|
// 解析API返回的开始时间
|
const startTimeStr = response.data?.startTime;
|
if (startTimeStr) {
|
const startTime = parseTimeString(startTimeStr);
|
if (startTime) {
|
setTaskStartTime(startTime);
|
setIsTaskTimeFallback(false);
|
console.log('Task start time loaded:', new Date(startTime).toLocaleString());
|
return;
|
}
|
}
|
|
throw new Error('无法解析开始时间');
|
} catch (err) {
|
console.error('Failed to load task time:', err);
|
|
// 降级到本地计时
|
const fallbackTime = getFallbackStartTime();
|
setTaskStartTime(fallbackTime);
|
setIsTaskTimeFallback(true);
|
message.warning('任务时间API不可用,使用本地计时');
|
console.log('Using fallback start time:', new Date(fallbackTime).toLocaleString());
|
}
|
};
|
/**
|
* 加载案件数据
|
*/
|
const loadCaseData = async (forceRefresh = false) => {
|
// 防止重复加载(除非强制刷新)
|
if (hasLoaded && !forceRefresh) {
|
console.log('Data already loaded, skipping...');
|
return;
|
}
|
|
setLoading(true);
|
setError(null);
|
|
try {
|
// 获取URL参数(合并默认值)
|
const params = getMergedParams();
|
|
console.log('Loading case data with params:', params);
|
|
if (!params.caseId) {
|
setError('caseId缺失');
|
setLoading(false);
|
return;
|
}
|
|
try {
|
await OutboundBotAPIService.syncStatusByCase({ caseId: params.caseId });
|
} catch (syncError) {
|
console.error('同步外呼状态失败:', syncError);
|
}
|
try {
|
await OutboundBotAPIService.backfillConversationByCase({ caseId: params.caseId });
|
} catch (backfillError) {
|
console.error('回补通话记录失败:', backfillError);
|
}
|
|
// 调用API获取数据
|
const response = await ProcessAPIService.getCaseProcessInfo(
|
params.caseId,
|
{
|
caseTypeFirst: params.caseTypeFirst,
|
platform_code: params.platform_code,
|
authorization: params.auth_token
|
}
|
);
|
|
// 提取timeline数据
|
const timelineData = response.timeline || response.data?.timeline || response;
|
|
// 提取nodes数据(确保为数组),兼容 nodeList 和 nodes 两种字段名
|
const nodesData = response.data?.nodeList || response.data?.nodes || response.nodes || [];
|
|
console.log('API Response:', response);
|
console.log('Extracted nodesData:', nodesData);
|
|
// 更新状态
|
setCaseData(timelineData);
|
setProcessNodes(Array.isArray(nodesData) ? nodesData : []); // 确保为数组
|
setHasLoaded(true); // 标记已加载
|
|
// 检查终态状态(调解成功/失败/人工接管),终态不执行外呼和存储
|
const mediationState = timelineData.mediation?.state;
|
const isTerminalState = [2, 3, 4].includes(mediationState);
|
|
if (isTerminalState) {
|
console.log('案件已处于终态状态:', mediationState, ',跳过外呼和存储');
|
return;
|
}
|
|
// 保存到localStorage
|
saveToStorage(timelineData);
|
|
// 触发智能外呼
|
await triggerOutboundCall(timelineData);
|
|
|
// 加载任务时间数据
|
await loadTaskTime(timelineData);
|
|
console.log('Case data loaded successfully:', timelineData);
|
} catch (err) {
|
console.error('Failed to load case data:', err);
|
setError(err.message || '加载案件数据失败');
|
|
// 显示错误提示
|
message.error('加载案件数据失败,请稍后重试');
|
|
// 使用Mock数据(缓存数据不包含nodes,所以统一使用Mock)
|
setHasLoaded(true);
|
|
} finally {
|
setLoading(false);
|
}
|
};
|
|
/**
|
* 刷新数据(强制从API获取)
|
*/
|
const refreshData = () => {
|
loadCaseData(true);
|
};
|
|
// 防止重复加载的引用标识
|
const loadInitRef = useRef(false);
|
|
/**
|
* 组件挂载时加载数据
|
*/
|
useEffect(() => {
|
console.log('===== CaseDataContext useEffect triggered =====');
|
console.log('loadInitRef.current:', loadInitRef.current);
|
console.log('hasLoaded:', hasLoaded);
|
console.log('processNodes:', processNodes);
|
|
// StrictMode下防止双重调用
|
if (!loadInitRef.current) {
|
loadInitRef.current = true;
|
console.log('Calling loadCaseData()...');
|
loadCaseData();
|
} else {
|
console.log('loadCaseData() skipped due to loadInitRef.current === true');
|
}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, []); // 只在挂载时执行一次
|
|
// 提供的值
|
const value = {
|
caseData,
|
processNodes, // 流程节点数据
|
loading,
|
error,
|
refreshData,
|
loadCaseData,
|
taskStartTime, // 任务开始时间
|
isTaskTimeFallback // 是否降级模式
|
};
|
|
return (
|
<CaseDataContext.Provider value={value}>
|
{children}
|
</CaseDataContext.Provider>
|
);
|
};
|
|
/**
|
* 自定义Hook:使用案件数据
|
* @returns {Object} { caseData, loading, error, refreshData, loadCaseData }
|
*/
|
export const useCaseData = () => {
|
const context = useContext(CaseDataContext);
|
|
if (!context) {
|
throw new Error('useCaseData must be used within CaseDataProvider');
|
}
|
|
return context;
|
};
|
|
export default CaseDataContext;
|