2025-07-19 16:47:52 +08:00
|
|
|
|
import { message } from 'ant-design-vue';
|
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
|
2026-01-07 14:22:01 +08:00
|
|
|
|
import { useAccessStore } from '@vben/stores';
|
|
|
|
|
|
|
2025-07-19 16:47:52 +08:00
|
|
|
|
// 创建 Axios 实例
|
|
|
|
|
|
const service = axios.create({
|
|
|
|
|
|
baseURL: '/im-api/',
|
|
|
|
|
|
timeout: 15_000,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 请求拦截器
|
|
|
|
|
|
service.interceptors.request.use(
|
|
|
|
|
|
(config) => {
|
|
|
|
|
|
const token = localStorage.getItem('token');
|
|
|
|
|
|
if (token) {
|
|
|
|
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return config;
|
|
|
|
|
|
},
|
|
|
|
|
|
(error) => {
|
|
|
|
|
|
console.error('Request error:', error);
|
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 响应拦截器
|
|
|
|
|
|
service.interceptors.response.use(
|
|
|
|
|
|
(response) => {
|
|
|
|
|
|
const { code, result, message: msg } = response.data;
|
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (code === 401) {
|
|
|
|
|
|
message.error(msg);
|
|
|
|
|
|
localStorage.removeItem('token');
|
|
|
|
|
|
localStorage.removeItem('chatUser');
|
|
|
|
|
|
window.location.href = '/login';
|
|
|
|
|
|
return Promise.reject(new Error(msg));
|
|
|
|
|
|
}
|
|
|
|
|
|
message.error(msg);
|
|
|
|
|
|
return Promise.reject(new Error(msg));
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
(error) => {
|
|
|
|
|
|
console.error('Response error:', error);
|
|
|
|
|
|
|
|
|
|
|
|
// 详细的错误处理
|
|
|
|
|
|
if (error.code === 'ECONNABORTED') {
|
|
|
|
|
|
console.log('请求超时,请检查网络连接');
|
|
|
|
|
|
} else if (error.response?.status === 0 || error.message.includes('CORS')) {
|
|
|
|
|
|
console.log('CORS错误检测到,尝试备用方案...');
|
|
|
|
|
|
handleCORSError(error.config);
|
|
|
|
|
|
} else if (error.response?.status >= 400 && error.response?.status < 500) {
|
|
|
|
|
|
console.log('客户端错误:', error.response.status, error.response.data);
|
|
|
|
|
|
} else if (error.response?.status >= 500) {
|
|
|
|
|
|
console.log('服务器错误:', error.response.status);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('网络错误:', error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message.error('网络请求失败');
|
|
|
|
|
|
return Promise.reject(error);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// CORS错误的备用处理方案
|
|
|
|
|
|
const handleCORSError = (config) => {
|
|
|
|
|
|
console.log('执行CORS错误备用方案...');
|
|
|
|
|
|
const apiUrl = config.url.replace('/api', '');
|
|
|
|
|
|
|
|
|
|
|
|
return fetch(apiUrl, {
|
|
|
|
|
|
method: config.method.toUpperCase(),
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
...config.headers,
|
|
|
|
|
|
},
|
|
|
|
|
|
mode: 'cors',
|
|
|
|
|
|
body: config.data ? JSON.stringify(config.data) : undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
console.error('备用方案也失败:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 封装请求方法
|
|
|
|
|
|
export const get = (url, params = {}) => {
|
|
|
|
|
|
return service.get(url, { params });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const post = (url, data = {}) => {
|
|
|
|
|
|
return service.post(url, data);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const upload = (url, file) => {
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
return service.post(url, formData, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'multipart/form-data',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 发送普通消息API
|
|
|
|
|
|
export const sendMessage = (data) => {
|
|
|
|
|
|
const apiUrl = '/im-api/send-to-user';
|
|
|
|
|
|
|
|
|
|
|
|
// 消息类型映射 - 根据用户提供的完整映射更新
|
2025-08-30 15:31:03 +08:00
|
|
|
|
const messageTypeMap: any = {
|
2025-07-19 16:47:52 +08:00
|
|
|
|
text: 0,
|
|
|
|
|
|
image: 1,
|
|
|
|
|
|
audio: 2,
|
|
|
|
|
|
video: 3,
|
|
|
|
|
|
prescription: 4,
|
|
|
|
|
|
'medical-record': 5,
|
|
|
|
|
|
'video-call': 6,
|
|
|
|
|
|
'audio-call': 7,
|
|
|
|
|
|
file: 8,
|
2026-01-07 14:22:01 +08:00
|
|
|
|
register: 10,
|
|
|
|
|
|
'patient-experience': 11,
|
|
|
|
|
|
'product-card': 12,
|
|
|
|
|
|
'end-consultation': 13,
|
2025-07-19 16:47:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 11:05:12 +08:00
|
|
|
|
// 确保 receiver_user_id 有 user- 前缀(如果是患者端)
|
|
|
|
|
|
const receiverId = data.receiverId.toString();
|
|
|
|
|
|
const normalizedReceiverId = receiverId.startsWith('user-') ? receiverId : `user-${receiverId}`;
|
|
|
|
|
|
|
2026-07-28 16:53:31 +08:00
|
|
|
|
// 确保 sender_user_id:小助手保持 doctor_assistant;医生端补 doctor- 前缀
|
2025-12-23 11:05:12 +08:00
|
|
|
|
const senderId = data.senderId.toString();
|
2026-07-28 16:53:31 +08:00
|
|
|
|
const normalizedSenderId =
|
|
|
|
|
|
senderId === 'doctor_assistant' || senderId.startsWith('doctor-')
|
|
|
|
|
|
? senderId
|
|
|
|
|
|
: `doctor-${senderId}`;
|
2025-12-23 11:05:12 +08:00
|
|
|
|
|
2025-07-19 16:47:52 +08:00
|
|
|
|
const requestData = {
|
|
|
|
|
|
room_id: data.roomId,
|
2025-12-23 11:05:12 +08:00
|
|
|
|
sender_user_id: normalizedSenderId,
|
|
|
|
|
|
receiver_user_id: normalizedReceiverId,
|
2025-07-19 16:47:52 +08:00
|
|
|
|
message_type: messageTypeMap[data.type] || 0,
|
|
|
|
|
|
message_content: data.content || '',
|
2025-07-22 16:55:09 +08:00
|
|
|
|
duration: data.duration || 0,
|
2025-07-19 16:47:52 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 如果包含通话信令,添加call_id和call_status
|
|
|
|
|
|
if (data.callId) {
|
|
|
|
|
|
requestData.call_id = data.callId;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.callStatus) {
|
|
|
|
|
|
requestData.call_status = data.callStatus;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-07 14:22:01 +08:00
|
|
|
|
// 从 accessStore 获取 Token
|
|
|
|
|
|
const accessStore = useAccessStore();
|
|
|
|
|
|
const token = accessStore.accessToken;
|
|
|
|
|
|
|
2025-07-19 16:47:52 +08:00
|
|
|
|
return axios
|
|
|
|
|
|
.post(apiUrl, requestData, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
2026-01-07 14:22:01 +08:00
|
|
|
|
'Authorization': token ? (token.startsWith('Bearer ') ? token : `Bearer ${token}`) : '',
|
2025-07-19 16:47:52 +08:00
|
|
|
|
},
|
|
|
|
|
|
timeout: 15_000,
|
|
|
|
|
|
withCredentials: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
console.log('消息发送成功:', response.data);
|
|
|
|
|
|
return response.data;
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
console.error('消息发送失败:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 专门的通话信令发送方法
|
|
|
|
|
|
export const sendCallSignal = (signalData) => {
|
|
|
|
|
|
const apiUrl = '/api/send-to-user';
|
|
|
|
|
|
|
|
|
|
|
|
// 验证必要参数
|
|
|
|
|
|
if (!signalData.sender_user_id) {
|
|
|
|
|
|
throw new Error('缺少发送方用户ID');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!signalData.receiver_user_id) {
|
|
|
|
|
|
throw new Error('缺少接收方用户ID');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!signalData.call_id) {
|
|
|
|
|
|
throw new Error('缺少通话ID');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!signalData.call_status) {
|
|
|
|
|
|
throw new Error('缺少通话状态');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 11:05:12 +08:00
|
|
|
|
// 确保 receiver_user_id 有 user- 前缀(如果是患者端)
|
|
|
|
|
|
const receiverId = signalData.receiver_user_id;
|
|
|
|
|
|
const normalizedReceiverId = receiverId.startsWith('user-') || receiverId.startsWith('doctor-')
|
|
|
|
|
|
? receiverId
|
|
|
|
|
|
: `user-${receiverId}`;
|
|
|
|
|
|
|
2025-07-19 16:47:52 +08:00
|
|
|
|
// 构建通话信令请求数据
|
|
|
|
|
|
const requestData = {
|
|
|
|
|
|
sender_user_id: signalData.sender_user_id,
|
2025-12-23 11:05:12 +08:00
|
|
|
|
receiver_user_id: normalizedReceiverId,
|
2025-07-19 16:47:52 +08:00
|
|
|
|
message_type: signalData.message_type || 6, // 默认视频通话
|
|
|
|
|
|
message_content:
|
|
|
|
|
|
signalData.message_content || JSON.stringify(signalData.data || {}),
|
|
|
|
|
|
call_id: signalData.call_id,
|
|
|
|
|
|
call_status: signalData.call_status,
|
|
|
|
|
|
// 添加来源标记
|
|
|
|
|
|
isFromHttp: true,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log('发送通话信令到API:', apiUrl, requestData);
|
|
|
|
|
|
|
2026-01-07 14:22:01 +08:00
|
|
|
|
// 从 accessStore 获取 Token
|
|
|
|
|
|
const accessStore = useAccessStore();
|
|
|
|
|
|
const token = accessStore.accessToken;
|
|
|
|
|
|
|
2025-07-19 16:47:52 +08:00
|
|
|
|
return axios
|
|
|
|
|
|
.post(apiUrl, requestData, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
2026-01-07 14:22:01 +08:00
|
|
|
|
'Authorization': token ? (token.startsWith('Bearer ') ? token : `Bearer ${token}`) : '',
|
2025-07-19 16:47:52 +08:00
|
|
|
|
},
|
|
|
|
|
|
timeout: 15_000,
|
|
|
|
|
|
withCredentials: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
console.log('通话信令发送成功:', response.data);
|
|
|
|
|
|
return response.data;
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
console.error('通话信令发送失败:', error);
|
|
|
|
|
|
|
|
|
|
|
|
// 提供更详细的错误信息
|
|
|
|
|
|
if (error.response) {
|
|
|
|
|
|
console.error('响应错误:', error.response.data);
|
|
|
|
|
|
console.error('响应状态:', error.response.status);
|
|
|
|
|
|
console.error('响应头:', error.response.headers);
|
|
|
|
|
|
} else if (error.request) {
|
|
|
|
|
|
console.error('请求错误:', error.request);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('配置错误:', error.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 通话相关的便捷方法
|
|
|
|
|
|
export const callAPI = {
|
|
|
|
|
|
// 发起通话邀请
|
|
|
|
|
|
invite: (senderUserId, receiverUserId, callId, callType = 6) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'invite',
|
|
|
|
|
|
message_type: callType, // 6=视频, 7=语音
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 接受通话
|
|
|
|
|
|
accept: (senderUserId, receiverUserId, callId, callType = 6) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'accepted',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 拒绝通话
|
|
|
|
|
|
reject: (senderUserId, receiverUserId, callId, callType = 6) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'rejected',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 结束通话
|
|
|
|
|
|
end: (senderUserId, receiverUserId, callId, callType = 6) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'ended',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 发送WebRTC Offer
|
|
|
|
|
|
sendOffer: (
|
|
|
|
|
|
senderUserId,
|
|
|
|
|
|
receiverUserId,
|
|
|
|
|
|
callId,
|
|
|
|
|
|
offerData,
|
|
|
|
|
|
callType = 6,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'offer',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
data: offerData,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 发送WebRTC Answer
|
|
|
|
|
|
sendAnswer: (
|
|
|
|
|
|
senderUserId,
|
|
|
|
|
|
receiverUserId,
|
|
|
|
|
|
callId,
|
|
|
|
|
|
answerData,
|
|
|
|
|
|
callType = 6,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'answer',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
data: answerData,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 发送ICE候选
|
|
|
|
|
|
sendCandidate: (
|
|
|
|
|
|
senderUserId,
|
|
|
|
|
|
receiverUserId,
|
|
|
|
|
|
callId,
|
|
|
|
|
|
candidateData,
|
|
|
|
|
|
callType = 6,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'candidate',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
data: candidateData,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 发送忙线状态
|
|
|
|
|
|
sendBusy: (senderUserId, receiverUserId, callId, callType = 6) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'busy',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 发送无人接听
|
|
|
|
|
|
sendNoAnswer: (senderUserId, receiverUserId, callId, callType = 6) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'no-answer',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 发送通话失败
|
|
|
|
|
|
sendFailed: (senderUserId, receiverUserId, callId, callType = 6) => {
|
|
|
|
|
|
return sendCallSignal({
|
|
|
|
|
|
sender_user_id: senderUserId,
|
|
|
|
|
|
receiver_user_id: receiverUserId,
|
|
|
|
|
|
call_id: callId,
|
|
|
|
|
|
call_status: 'failed',
|
|
|
|
|
|
message_type: callType,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default service;
|