import Taro from '@tarojs/taro';
|
import { getStorage } from './tools';
|
import { _UID } from '../constants/api';
|
import { safeParse } from './Robotaxi-public-method';
|
|
type MessageCallback = (data: any) => void;
|
|
class WebSocketManager {
|
private static instance: WebSocketManager;
|
private socketTask: Taro.SocketTask | null = null;
|
private url: string = '';
|
private isConnected: boolean = false;
|
private isConnecting: boolean = false;
|
private isManualClose: boolean = false;
|
private reconnectAttempts: number = 0;
|
private maxReconnectAttempts: number = 5;
|
private reconnectTimer: any = null;
|
private heartbeatTimer: any = null;
|
private heartbeatTimeoutTimer: any = null; // 用于检测心跳超时的定时器
|
private heartbeatInterval: number = 10000; // 10秒心跳
|
private heartbeatTimeout: number = 3000; // 5秒收不到响应即视为掉线
|
private listeners: Map<string, MessageCallback[]> = new Map();
|
private globalEventsBound: boolean = false;
|
|
private constructor() { }
|
|
public static getInstance(): WebSocketManager {
|
if (!WebSocketManager.instance) {
|
WebSocketManager.instance = new WebSocketManager();
|
}
|
return WebSocketManager.instance;
|
}
|
|
/**
|
* 初始化连接
|
* @param url WebSocket地址
|
*/
|
public async connect(url: string) {
|
if (this.socketTask || this.isConnected || this.isConnecting) {
|
console.log('WebSocket 已连接或正在连接中');
|
return;
|
}
|
|
this.url = url;
|
this.isConnecting = true;
|
this.isManualClose = false;
|
console.log(`正在连接 WebSocket: ${url}`);
|
|
const uid = await getStorage(_UID) || '-1';
|
const task = Taro.connectSocket({
|
header: {
|
'XP-Uid': uid.toString(),
|
},
|
url: this.url,
|
success: () => { },
|
fail: (err) => {
|
console.error('WebSocket 连接失败:', err);
|
this.isConnecting = false;
|
this.handleReconnect();
|
}
|
});
|
const maybeTask: any = task as any;
|
if (maybeTask && typeof maybeTask.onOpen === 'function') {
|
this.socketTask = maybeTask;
|
this.isConnecting = false;
|
this.initEventListeners();
|
return;
|
}
|
if (maybeTask && typeof maybeTask.then === 'function') {
|
maybeTask
|
.then((resolvedTask) => {
|
this.socketTask = resolvedTask;
|
this.isConnecting = false;
|
this.initEventListeners();
|
})
|
.catch((err) => {
|
console.error('WebSocket 连接失败:', err);
|
this.isConnecting = false;
|
this.handleReconnect();
|
});
|
return;
|
}
|
this.socketTask = null;
|
this.isConnecting = false;
|
this.initEventListeners();
|
}
|
|
public isConnectedStatus() {
|
return this.isConnected;
|
}
|
|
/**
|
* 初始化事件监听
|
*/
|
private initEventListeners() {
|
if (this.socketTask && typeof this.socketTask.onOpen === 'function') {
|
this.socketTask.onOpen(async () => {
|
console.log('WebSocket 连接已打开');
|
this.isConnected = true;
|
this.isConnecting = false;
|
this.reconnectAttempts = 0;
|
this.startHeartbeat();
|
const uid = await getStorage(_UID) || '-1';
|
this.send({ type: 'auth', appUserId: uid.toString(), timestamp: Date.now() });
|
this.notifyListeners('open', { status: 'connected' });
|
});
|
|
this.socketTask.onMessage((res) => {
|
this.resetHeartbeatTimeout();
|
let data: any = {};
|
try {
|
data = safeParse(res.data as string, {});
|
console.log('收到 WebSocket 消息:', res.data);
|
} catch (e) {
|
console.warn('解析 WebSocket data 失败:', res.data);
|
this.notifyListeners('message', res.data);
|
return;
|
}
|
|
const content = safeParse(data.content || '{}', {});
|
|
try {
|
this.handleIncomingMessage(data, content);
|
if (data.type) {
|
this.notifyListeners(data.type, data);
|
}
|
this.notifyListeners('message', data);
|
} catch (e) {
|
console.error('处理 WebSocket 消息出错:', e);
|
}
|
});
|
|
this.socketTask.onClose((res) => {
|
console.log('WebSocket 连接已关闭', res);
|
this.isConnected = false;
|
this.isConnecting = false;
|
this.stopHeartbeat();
|
this.socketTask = null;
|
|
if (!this.isManualClose) {
|
this.handleReconnect();
|
}
|
this.notifyListeners('close', res);
|
});
|
|
this.socketTask.onError((err) => {
|
console.error('WebSocket 错误:', err);
|
this.isConnected = false;
|
this.isConnecting = false;
|
this.notifyListeners('error', err);
|
});
|
return;
|
}
|
|
if (this.globalEventsBound) return;
|
this.globalEventsBound = true;
|
|
Taro.onSocketOpen(async () => {
|
console.log('WebSocket 连接已打开');
|
this.isConnected = true;
|
this.isConnecting = false;
|
this.reconnectAttempts = 0;
|
this.startHeartbeat();
|
const uid = await getStorage(_UID) || '-1';
|
this.send({ type: 'auth', appUserId: uid.toString(), timestamp: Date.now() });
|
this.notifyListeners('open', { status: 'connected' });
|
});
|
|
Taro.onSocketMessage((res) => {
|
this.resetHeartbeatTimeout();
|
let data: any = {};
|
try {
|
data = safeParse(res.data as string, {});
|
console.log('收到 WebSocket 消息:', res.data);
|
} catch (e) {
|
console.warn('解析 WebSocket data 失败:', res.data);
|
this.notifyListeners('message', res.data);
|
return;
|
}
|
|
const content = safeParse(data.content || '{}', {});
|
|
try {
|
this.handleIncomingMessage(data, content);
|
if (data.type) {
|
this.notifyListeners(data.type, data);
|
}
|
this.notifyListeners('message', data);
|
} catch (e) {
|
console.error('处理 WebSocket 消息出错:', e);
|
}
|
});
|
|
Taro.onSocketClose((res) => {
|
console.log('WebSocket 连接已关闭', res);
|
this.isConnected = false;
|
this.isConnecting = false;
|
this.stopHeartbeat();
|
this.socketTask = null;
|
|
if (!this.isManualClose) {
|
this.handleReconnect();
|
}
|
this.notifyListeners('close', res);
|
});
|
|
Taro.onSocketError((err) => {
|
console.error('WebSocket 错误:', err);
|
this.isConnected = false;
|
this.isConnecting = false;
|
this.notifyListeners('error', err);
|
});
|
}
|
|
/**
|
* 发送消息
|
* @param data 消息内容
|
*/
|
public send(data: any) {
|
if (!this.isConnected) {
|
console.warn('WebSocket 未连接,消息未发送:', data);
|
return;
|
}
|
|
const message = typeof data === 'string' ? data : JSON.stringify(data);
|
if (this.socketTask) {
|
this.socketTask.send({
|
data: message,
|
fail: (err) => {
|
console.error('发送消息失败:', err);
|
}
|
});
|
} else {
|
Taro.sendSocketMessage({
|
data: message,
|
fail: (err) => {
|
console.error('发送消息失败:', err);
|
}
|
});
|
}
|
}
|
|
/**
|
* 关闭连接
|
*/
|
public close() {
|
this.isManualClose = true;
|
this.stopHeartbeat();
|
if (this.reconnectTimer) {
|
clearTimeout(this.reconnectTimer);
|
this.reconnectTimer = null;
|
}
|
|
if (this.socketTask) {
|
this.socketTask.close({
|
code: 1000,
|
reason: '用户主动关闭连接'
|
});
|
this.socketTask = null;
|
}
|
this.isConnected = false;
|
this.isConnecting = false;
|
}
|
|
/**
|
* 处理重连
|
*/
|
private handleReconnect() {
|
if (this.reconnectTimer) return;
|
|
// 固定约 3 秒重连一次,并加上 0~1 秒的随机抖动
|
const delay = 3000 + Math.floor(Math.random() * 1000);
|
console.log(`正在尝试重连,延迟 ${delay}ms... (第 ${this.reconnectAttempts + 1} 次尝试)`);
|
|
this.reconnectTimer = setTimeout(() => {
|
// 防止数字溢出(无限重试时)
|
if (this.reconnectAttempts < 9999) {
|
this.reconnectAttempts++;
|
}
|
this.reconnectTimer = null;
|
this.connect(this.url);
|
}, delay);
|
}
|
|
/**
|
* 开启心跳
|
*/
|
private startHeartbeat() {
|
this.stopHeartbeat();
|
this.heartbeatTimer = setInterval(() => {
|
if (this.isConnected) {
|
this.send({ type: 'ping', timestamp: Date.now() });
|
// 发送 ping 后,启动一个超时检测
|
// 如果在配置的时间内没有收到任何消息(pong 或业务消息),则认为连接已假死
|
if (this.heartbeatTimeoutTimer) {
|
clearTimeout(this.heartbeatTimeoutTimer);
|
}
|
this.heartbeatTimeoutTimer = setTimeout(() => {
|
console.warn('心跳超时,未收到服务器响应,主动断开 WebSocket 连接');
|
this.closeAndReconnect();
|
}, this.heartbeatTimeout);
|
}
|
}, this.heartbeatInterval);
|
}
|
|
/**
|
* 停止心跳
|
*/
|
private stopHeartbeat() {
|
if (this.heartbeatTimer) {
|
clearInterval(this.heartbeatTimer);
|
this.heartbeatTimer = null;
|
}
|
if (this.heartbeatTimeoutTimer) {
|
clearTimeout(this.heartbeatTimeoutTimer);
|
this.heartbeatTimeoutTimer = null;
|
}
|
}
|
|
/**
|
* 收到消息时重置心跳超时检测
|
*/
|
private resetHeartbeatTimeout() {
|
if (this.heartbeatTimeoutTimer) {
|
clearTimeout(this.heartbeatTimeoutTimer);
|
this.heartbeatTimeoutTimer = null;
|
}
|
}
|
|
/**
|
* 主动关闭并重连 (处理假死状态)
|
*/
|
private closeAndReconnect() {
|
// 强制关闭当前连接
|
if (this.socketTask) {
|
try {
|
this.socketTask.close({
|
code: 1000,
|
reason: '心跳超时主动关闭'
|
});
|
} catch (e) {
|
console.error('强制关闭 Socket 失败:', e);
|
}
|
this.socketTask = null;
|
}
|
this.isConnected = false;
|
this.isConnecting = false;
|
this.stopHeartbeat();
|
|
// 立即触发重连
|
if (!this.isManualClose) {
|
this.handleReconnect();
|
}
|
}
|
|
/**
|
* 注册事件监听
|
* @param event 事件名称
|
* @param callback 回调函数
|
*/
|
public on(event: string, callback: MessageCallback) {
|
if (!this.listeners.has(event)) {
|
this.listeners.set(event, []);
|
}
|
this.listeners.get(event)?.push(callback);
|
}
|
|
/**
|
* 移除事件监听
|
* @param event 事件名称
|
* @param callback 回调函数
|
*/
|
public off(event: string, callback: MessageCallback) {
|
const callbacks = this.listeners.get(event);
|
if (callbacks) {
|
const index = callbacks.indexOf(callback);
|
if (index !== -1) {
|
callbacks.splice(index, 1);
|
}
|
}
|
}
|
|
/**
|
* 通知监听器
|
* @param event 事件名称
|
* @param data 数据
|
*/
|
private notifyListeners(event: string, data: any) {
|
const callbacks = this.listeners.get(event);
|
if (callbacks) {
|
callbacks.forEach(callback => {
|
try {
|
callback(data);
|
} catch (e) {
|
console.error(`WebSocket 事件 ${event} 监听器执行出错:`, e);
|
}
|
});
|
}
|
}
|
|
// 处理传入消息
|
private handleIncomingMessage(data: any, content: any) {
|
if (data?.type === 'ORDER_STATUS') {
|
const status = content?.status;
|
const orderId = content?.orderId;
|
if (typeof status === 'number') {
|
const mapped = this.mapOrderStatusToTripStatus(status);
|
if (typeof mapped.tripStatus === 'number') {
|
Taro.setStorageSync('tripStatus', mapped.tripStatus);
|
Taro.setStorageSync('orderId', orderId);
|
}
|
this.notifyListeners('order_status', { status, ...mapped, data, content });
|
}
|
}
|
if (data?.type === 'ORDER_ROUTE') {
|
const routeType = content?.routeType;
|
const direction = Number(content?.direction);
|
const duration = Number(safeParse(content?.route ?? '{}', {})?.duration ?? '0');
|
const distance = Number(safeParse(content?.route ?? '{}', {})?.distance ?? '0');
|
const points = this.normalizeRoutePoints(content);
|
if (!Number.isNaN(duration)) {
|
Taro.setStorageSync('orderRouteDuration', Math.round(duration));
|
Taro.setStorageSync('orderRouteDurationUnit', 's');
|
}
|
if (!Number.isNaN(distance)) {
|
const distanceKm = Number((distance / 1000).toFixed(1));
|
Taro.setStorageSync('orderRouteDistance', distanceKm);
|
Taro.setStorageSync('orderRouteDistanceUnit', 'km');
|
}
|
if (points.length > 0) {
|
const cached = Taro.getStorageSync('orderRoutePoints') || {};
|
if (typeof routeType === 'number') {
|
cached[String(routeType)] = points;
|
Taro.setStorageSync('orderRouteLatestType', routeType);
|
} else {
|
cached.latest = points;
|
}
|
Taro.setStorageSync('orderRoutePoints', cached);
|
this.notifyListeners('order_route', { routeType, direction, points, data, content });
|
}
|
}
|
if (data?.type === 'ORDER_UPDATE_PRICE') {
|
const orderId = content?.orderId ?? data?.orderId;
|
this.notifyListeners('order_update_price', { orderId, data, content });
|
}
|
if (data?.type === 'ORDER_REASSIGN') {
|
const orderId = content?.orderId ?? data?.orderId;
|
this.notifyListeners('order_reassign', { orderId, data, content });
|
}
|
if (data?.type === 'OPEN_TRUNK') {
|
const isOpen = content?.isOpen ?? content?.open;
|
this.notifyListeners('open_trunk', { isOpen, data, content });
|
}
|
}
|
|
// 归一化路线点
|
private normalizeRoutePoints(content: any) {
|
const route = safeParse(content?.route ?? '{}', {});
|
const rawPoints = route?.polyline ?? [];
|
const arrPoint = safeParse(rawPoints, []);
|
if (!Array.isArray(arrPoint)) return [];
|
return arrPoint.map((item: any) => {
|
const latitude = item?.latitude;
|
const longitude = item?.longitude;
|
if (typeof latitude === 'number' && typeof longitude === 'number') {
|
return { latitude, longitude };
|
}
|
return null;
|
}).filter(Boolean);
|
}
|
|
// 映射订单状态到行程状态
|
private mapOrderStatusToTripStatus(status: number) {
|
switch (status) {
|
case 0:
|
return { tripStatus: 1, showCancelPopup: true };
|
case 1:
|
return { tripStatus: 3, showPassengerNotice: true };
|
case 2:
|
case 3:
|
return { tripStatus: 4 };
|
case 4:
|
return { tripStatus: 5, showAnnouncementsPopup: true };
|
case 5:
|
case 6:
|
return { tripStatus: 6 };
|
case 7:
|
return { tripStatus: 7 };
|
case 8:
|
return { tripStatus: 8 };
|
case 9:
|
case 10:
|
return { tripStatus: 9 };
|
default:
|
return { tripStatus: undefined };
|
}
|
}
|
}
|
|
export default WebSocketManager.getInstance();
|