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;
|
|
enum SocketState {
|
IDLE = 'idle',
|
CONNECTING = 'connecting',
|
CONNECTED = 'connected',
|
}
|
|
class WebSocketManager {
|
private static instance: WebSocketManager;
|
private socketTask: Taro.SocketTask | null = null; // 当前生效的 socketTask
|
private url: string = ''; // 当前连接地址
|
private socketState: SocketState = SocketState.IDLE; // 当前连接状态
|
private isConnected: boolean = false; // 对外兼容的连接态
|
private isConnecting: boolean = false; // 对外兼容的连接中标记
|
private isManualClose: boolean = false; // 是否手动关闭
|
private isAppActive: boolean = true; // 应用是否处于前台
|
private shouldReconnectWhenActive: boolean = false; // 回前台后是否需要补重连
|
private reconnectAttempts: number = 0; // 当前连续重连次数
|
private reconnectTimer: any = null; // 重连定时器
|
private heartbeatTimer: any = null; // 心跳发送定时器
|
private heartbeatTimeoutTimer: any = null; // 单次等待 pong 的超时定时器
|
private heartbeatInterval: number = 15000; // 心跳间隔
|
private heartbeatTimeout: number = 10000; // 单次心跳等待 pong 的超时时间
|
private maxHeartbeatMisses: number = 2; // 连续丢失 pong 的最大次数
|
private locationReportTimer: any = null; // 位置上报定时器
|
private locationReportInterval: number = 35000; // 35 秒上报一次位置
|
private listeners: Map<string, MessageCallback[]> = new Map(); // 事件监听器
|
private globalEventsBound: boolean = false; // 全局事件是否已绑定
|
private connectionSeed: number = 0; // 连接代次种子
|
private activeConnectionId: number = 0; // 当前生效连接代次
|
private heartbeatSeq: number = 0; // 心跳序号
|
private pendingHeartbeatSeq: number | null = null; // 当前等待 pong 的心跳序号
|
private missedHeartbeatCount: number = 0; // 连续未收到 pong 的次数
|
|
private constructor() { }
|
|
public static getInstance(): WebSocketManager {
|
if (!WebSocketManager.instance) {
|
WebSocketManager.instance = new WebSocketManager();
|
}
|
return WebSocketManager.instance;
|
}
|
|
/**
|
* 应用回到前台时恢复心跳或补发重连。
|
*/
|
public onAppShow() {
|
this.isAppActive = true;
|
this.shouldReconnectWhenActive = false;
|
if (this.isConnected && this.activeConnectionId > 0) {
|
this.startHeartbeat(this.activeConnectionId);
|
this.startLocationReport(this.activeConnectionId);
|
return;
|
}
|
if (!this.isManualClose && this.url) {
|
this.connect(this.url);
|
}
|
}
|
|
/**
|
* 应用进入后台时暂停心跳,避免后台定时器抖动误判掉线。
|
*/
|
public onAppHide() {
|
this.isAppActive = false;
|
this.stopHeartbeat();
|
this.stopLocationReport();
|
if (this.reconnectTimer) {
|
clearTimeout(this.reconnectTimer);
|
this.reconnectTimer = null;
|
this.shouldReconnectWhenActive = true;
|
}
|
}
|
|
/**
|
* 初始化连接。
|
* @param url WebSocket 地址
|
*/
|
public async connect(url: string) {
|
if (!url) {
|
return;
|
}
|
if (this.isConnected && this.url === url) {
|
return;
|
}
|
if (this.isConnecting && this.url === url) {
|
return;
|
}
|
|
this.url = url;
|
this.isManualClose = false;
|
this.shouldReconnectWhenActive = false;
|
this.isConnecting = true;
|
this.socketState = SocketState.CONNECTING;
|
this.clearReconnectTimer();
|
|
const connectionId = this.createConnectionId();
|
console.log(`正在连接 WebSocket: ${url},连接代次:${connectionId}`);
|
|
try {
|
const uid = await this.getValidUid();
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
if (!uid) {
|
console.warn('WebSocket 连接已取消,UID 无效');
|
this.markConnectionClosed(connectionId);
|
return;
|
}
|
|
const task = Taro.connectSocket({
|
header: {
|
'XP-Uid': uid,
|
},
|
url: this.url,
|
success: () => { },
|
fail: (err) => {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
console.error('WebSocket 连接失败:', err);
|
this.markConnectionClosed(connectionId);
|
this.scheduleReconnect(connectionId, 'connect_fail');
|
}
|
});
|
|
const maybeTask: any = task as any;
|
if (maybeTask && typeof maybeTask.onOpen === 'function') {
|
this.attachTask(connectionId, maybeTask);
|
return;
|
}
|
|
if (maybeTask && typeof maybeTask.then === 'function') {
|
maybeTask
|
.then((resolvedTask: Taro.SocketTask) => {
|
if (!this.isCurrentConnection(connectionId)) {
|
this.closeTaskSilently(resolvedTask, '过期连接任务关闭');
|
return;
|
}
|
this.attachTask(connectionId, resolvedTask);
|
})
|
.catch((err: any) => {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
console.error('WebSocket 连接失败:', err);
|
this.markConnectionClosed(connectionId);
|
this.scheduleReconnect(connectionId, 'connect_promise_fail');
|
});
|
return;
|
}
|
|
this.bindGlobalEventListeners();
|
} catch (error) {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
console.error('初始化 WebSocket 连接失败:', error);
|
this.markConnectionClosed(connectionId);
|
this.scheduleReconnect(connectionId, 'connect_exception');
|
}
|
}
|
|
/**
|
* 对外暴露当前连接状态。
|
*/
|
public isConnectedStatus() {
|
return this.isConnected;
|
}
|
|
/**
|
* 发送消息。
|
* @param data 消息内容
|
*/
|
public send(data: any) {
|
this.sendInternal(data, { silentWhenDisconnected: false });
|
}
|
|
/**
|
* 主动关闭连接。
|
*/
|
public close() {
|
const closedConnectionId = this.activeConnectionId;
|
this.isManualClose = true;
|
this.shouldReconnectWhenActive = false;
|
this.clearReconnectTimer();
|
this.stopHeartbeat();
|
this.stopLocationReport();
|
this.invalidateCurrentConnection();
|
this.closeTaskSilently(this.socketTask, '用户主动关闭连接');
|
this.socketTask = null;
|
this.markConnectionClosed(closedConnectionId);
|
}
|
|
/**
|
* 注册事件监听。
|
* @param event 事件名称
|
* @param callback 回调函数
|
*/
|
public on(event: string, callback: MessageCallback) {
|
if (!this.listeners.has(event)) {
|
this.listeners.set(event, []);
|
}
|
const callbacks = this.listeners.get(event)!;
|
if (!callbacks.includes(callback)) {
|
callbacks.push(callback);
|
}
|
}
|
|
/**
|
* 移除事件监听。
|
* @param event 事件名称
|
* @param callback 回调函数
|
*/
|
public off(event: string, callback: MessageCallback) {
|
const callbacks = this.listeners.get(event);
|
if (callbacks) {
|
let index = callbacks.indexOf(callback);
|
while (index !== -1) {
|
callbacks.splice(index, 1);
|
index = callbacks.indexOf(callback);
|
}
|
}
|
}
|
|
/**
|
* 创建新的连接代次,并让旧连接回调全部失效。
|
*/
|
private createConnectionId() {
|
this.connectionSeed += 1;
|
this.activeConnectionId = this.connectionSeed;
|
return this.activeConnectionId;
|
}
|
|
/**
|
* 当前连接失效后让旧回调不再影响全局状态。
|
*/
|
private invalidateCurrentConnection() {
|
this.connectionSeed += 1;
|
this.activeConnectionId = this.connectionSeed;
|
this.pendingHeartbeatSeq = null;
|
}
|
|
/**
|
* 判断回调是否仍然属于当前连接代次。
|
*/
|
private isCurrentConnection(connectionId: number) {
|
return connectionId > 0 && connectionId === this.activeConnectionId;
|
}
|
|
/**
|
* 绑定当前 socketTask,并注册基于连接代次的回调。
|
*/
|
private attachTask(connectionId: number, task: Taro.SocketTask) {
|
if (!this.isCurrentConnection(connectionId)) {
|
this.closeTaskSilently(task, '过期连接任务关闭');
|
return;
|
}
|
this.socketTask = task;
|
this.bindTaskEventListeners(connectionId, task);
|
}
|
|
/**
|
* 绑定 task 级别事件,确保旧连接回调不会污染新连接。
|
*/
|
private bindTaskEventListeners(connectionId: number, task: Taro.SocketTask) {
|
task.onOpen(() => {
|
this.handleSocketOpen(connectionId);
|
});
|
|
task.onMessage((res) => {
|
this.handleSocketMessage(connectionId, res.data);
|
});
|
|
task.onClose((res) => {
|
this.handleSocketClose(connectionId, res);
|
});
|
|
task.onError((err) => {
|
this.handleSocketError(connectionId, err);
|
});
|
}
|
|
/**
|
* 兼容无 task 级事件的运行环境。
|
*/
|
private bindGlobalEventListeners() {
|
if (this.globalEventsBound) return;
|
this.globalEventsBound = true;
|
|
Taro.onSocketOpen(() => {
|
this.handleSocketOpen(this.activeConnectionId);
|
});
|
|
Taro.onSocketMessage((res) => {
|
this.handleSocketMessage(this.activeConnectionId, res.data);
|
});
|
|
Taro.onSocketClose((res) => {
|
this.handleSocketClose(this.activeConnectionId, res);
|
});
|
|
Taro.onSocketError((err) => {
|
this.handleSocketError(this.activeConnectionId, err);
|
});
|
}
|
|
/**
|
* 处理连接建立成功。
|
*/
|
private async handleSocketOpen(connectionId: number) {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
console.log(`WebSocket 连接已打开,连接代次:${connectionId}`);
|
this.isConnected = true;
|
this.isConnecting = false;
|
this.socketState = SocketState.CONNECTED;
|
this.reconnectAttempts = 0;
|
this.resetHeartbeatState();
|
if (this.isAppActive) {
|
this.startHeartbeat(connectionId);
|
this.startLocationReport(connectionId);
|
}
|
|
const uid = await this.getValidUid();
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
if (!uid) {
|
console.warn('WebSocket 鉴权已取消,UID 无效');
|
const expiredTask = this.socketTask;
|
this.stopHeartbeat();
|
this.invalidateCurrentConnection();
|
this.socketTask = null;
|
this.closeTaskSilently(expiredTask, 'UID 无效,关闭未鉴权连接');
|
this.markConnectionClosed(connectionId);
|
return;
|
}
|
this.sendInternal(
|
{ type: 'auth', appUserId: uid, timestamp: Date.now() },
|
{ silentWhenDisconnected: true }
|
);
|
this.notifyListeners('open', { status: 'connected' });
|
}
|
|
/**
|
* 处理服务端消息。
|
*/
|
private handleSocketMessage(connectionId: number, rawData: any) {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
|
// console.log('收到 WebSocket 消息:', rawData);
|
const data = this.normalizeIncomingMessage(rawData);
|
if (this.isPongMessage(data)) {
|
this.handlePong(connectionId);
|
}
|
|
const content = safeParse(data?.content || '{}', {});
|
|
try {
|
this.handleIncomingMessage(data, content);
|
if (data?.type) {
|
this.notifyListeners(data.type, data);
|
}
|
this.notifyListeners('message', data);
|
} catch (error) {
|
console.error('处理 WebSocket 消息出错:', error);
|
}
|
}
|
|
/**
|
* 处理连接关闭。
|
*/
|
private handleSocketClose(connectionId: number, res: any) {
|
if (!this.isCurrentConnection(connectionId)) {
|
console.log('忽略过期连接关闭回调', res);
|
return;
|
}
|
console.log('WebSocket 连接已关闭', res);
|
this.markConnectionClosed(connectionId);
|
this.notifyListeners('close', res);
|
if (!this.isManualClose) {
|
this.scheduleReconnect(connectionId, 'socket_close');
|
}
|
}
|
|
/**
|
* 处理连接异常。
|
*/
|
private handleSocketError(connectionId: number, err: any) {
|
if (!this.isCurrentConnection(connectionId)) {
|
console.log('忽略过期连接错误回调', err);
|
return;
|
}
|
console.error('WebSocket 错误:', err);
|
this.markConnectionClosed(connectionId);
|
this.notifyListeners('error', err);
|
if (!this.isManualClose) {
|
this.scheduleReconnect(connectionId, 'socket_error');
|
}
|
}
|
|
/**
|
* 标准化消息结构,兼容纯字符串 pong。
|
*/
|
private normalizeIncomingMessage(rawData: any) {
|
const parsedData = safeParse(rawData, rawData);
|
if (typeof parsedData === 'string') {
|
return {
|
type: parsedData,
|
raw: rawData,
|
};
|
}
|
return parsedData || {};
|
}
|
|
/**
|
* 获取有效 UID,约束连接头和鉴权消息都不能使用 -1 等无效值。
|
*/
|
private async getValidUid() {
|
const uid = await getStorage(_UID);
|
return this.normalizeValidUid(uid);
|
}
|
|
/**
|
* 标准化并校验 UID。
|
*/
|
private normalizeValidUid(uid: any) {
|
const normalizedUid = String(uid ?? '').trim();
|
if (!normalizedUid || normalizedUid === '-1' || normalizedUid === 'undefined' || normalizedUid === 'null') {
|
return null;
|
}
|
return normalizedUid;
|
}
|
|
/**
|
* 判断消息是否为服务端 pong。
|
*/
|
private isPongMessage(data: any) {
|
const type = String(data?.type || '').toLowerCase();
|
return type === 'pong';
|
}
|
|
/**
|
* 根据 pong 清理等待状态,并恢复心跳健康计数。
|
*/
|
private handlePong(connectionId: number) {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
this.pendingHeartbeatSeq = null;
|
this.missedHeartbeatCount = 0;
|
if (this.heartbeatTimeoutTimer) {
|
clearTimeout(this.heartbeatTimeoutTimer);
|
this.heartbeatTimeoutTimer = null;
|
}
|
}
|
|
/**
|
* 启动心跳轮询。
|
*/
|
private startHeartbeat(connectionId: number) {
|
this.stopHeartbeat();
|
if (!this.isCurrentConnection(connectionId) || !this.isAppActive) {
|
return;
|
}
|
this.heartbeatTimer = setInterval(() => {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
if (!this.isConnected || this.socketState !== SocketState.CONNECTED || !this.isAppActive) {
|
return;
|
}
|
if (this.pendingHeartbeatSeq !== null) {
|
return;
|
}
|
void this.sendHeartbeat(connectionId);
|
}, this.heartbeatInterval);
|
}
|
|
/**
|
* 发送一轮 ping,并等待服务端回 pong。
|
*/
|
private async sendHeartbeat(connectionId: number) {
|
if (!this.isCurrentConnection(connectionId) || !this.isAppActive) {
|
return;
|
}
|
const appUserId = await this.getValidUid();
|
if (!this.isCurrentConnection(connectionId) || !this.isAppActive) {
|
return;
|
}
|
const currentHeartbeatSeq = ++this.heartbeatSeq;
|
this.pendingHeartbeatSeq = currentHeartbeatSeq;
|
const hasSent = this.sendInternal(
|
{ type: 'ping', appUserId, timestamp: Date.now(), seq: currentHeartbeatSeq },
|
{ silentWhenDisconnected: true }
|
);
|
|
if (!hasSent) {
|
this.handleHeartbeatMiss(connectionId, currentHeartbeatSeq, 'ping_send_fail');
|
return;
|
}
|
|
this.heartbeatTimeoutTimer = setTimeout(() => {
|
this.handleHeartbeatMiss(connectionId, currentHeartbeatSeq, 'pong_timeout');
|
}, this.heartbeatTimeout);
|
}
|
|
/**
|
* 处理单轮心跳未收到 pong 的场景。
|
*/
|
private handleHeartbeatMiss(connectionId: number, heartbeatSeq: number, reason: string) {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
if (!this.isAppActive) {
|
this.pendingHeartbeatSeq = null;
|
return;
|
}
|
if (this.pendingHeartbeatSeq !== heartbeatSeq) {
|
return;
|
}
|
|
this.pendingHeartbeatSeq = null;
|
if (this.heartbeatTimeoutTimer) {
|
clearTimeout(this.heartbeatTimeoutTimer);
|
this.heartbeatTimeoutTimer = null;
|
}
|
this.missedHeartbeatCount += 1;
|
console.warn(`WebSocket 心跳未收到 pong,原因:${reason},连续失败 ${this.missedHeartbeatCount} 次`);
|
|
if (this.missedHeartbeatCount < this.maxHeartbeatMisses) {
|
return;
|
}
|
|
this.safeReconnect(connectionId, reason);
|
}
|
|
/**
|
* 安全关闭当前连接并发起重连,不让旧连接回调影响新连接。
|
*/
|
private safeReconnect(connectionId: number, reason: string) {
|
if (!this.isCurrentConnection(connectionId)) {
|
return;
|
}
|
|
console.warn(`WebSocket 准备重连,原因:${reason},连接代次:${connectionId}`);
|
const expiredTask = this.socketTask;
|
this.stopHeartbeat();
|
this.markConnectionClosed(connectionId);
|
this.invalidateCurrentConnection();
|
this.socketTask = null;
|
this.closeTaskSilently(expiredTask, `重连前关闭旧连接:${reason}`);
|
this.scheduleReconnect(this.activeConnectionId, reason);
|
}
|
|
/**
|
* 停止心跳及其超时检测。
|
*/
|
private stopHeartbeat() {
|
if (this.heartbeatTimer) {
|
clearInterval(this.heartbeatTimer);
|
this.heartbeatTimer = null;
|
}
|
if (this.heartbeatTimeoutTimer) {
|
clearTimeout(this.heartbeatTimeoutTimer);
|
this.heartbeatTimeoutTimer = null;
|
}
|
this.pendingHeartbeatSeq = null;
|
}
|
|
/**
|
* 启动位置上报轮询。
|
*/
|
private startLocationReport(connectionId: number) {
|
this.stopLocationReport();
|
if (!this.isCurrentConnection(connectionId) || !this.isAppActive || !this.isConnected) {
|
return;
|
}
|
this.sendLocationMessage(connectionId);
|
this.locationReportTimer = setInterval(() => {
|
if (!this.isCurrentConnection(connectionId) || !this.isAppActive || !this.isConnected) {
|
return;
|
}
|
this.sendLocationMessage(connectionId);
|
}, this.locationReportInterval);
|
}
|
|
/**
|
* 停止位置上报轮询。
|
*/
|
private stopLocationReport() {
|
if (this.locationReportTimer) {
|
clearInterval(this.locationReportTimer);
|
this.locationReportTimer = null;
|
}
|
}
|
|
/**
|
* 上报当前位置,失败时静默处理。
|
*/
|
private async sendLocationMessage(connectionId: number) {
|
if (!this.isCurrentConnection(connectionId) || !this.isConnected || !this.isAppActive) {
|
return;
|
}
|
const userId = await this.getValidUid();
|
if (!userId) {
|
return;
|
}
|
try {
|
const locationRes = await Taro.getLocation({
|
type: 'gcj02',
|
isHighAccuracy: true,
|
highAccuracyExpireTime: 3000,
|
} as any);
|
if (!this.isCurrentConnection(connectionId) || !this.isConnected || !this.isAppActive) {
|
return;
|
}
|
const longitude = Number(locationRes?.longitude);
|
const latitude = Number(locationRes?.latitude);
|
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
|
return;
|
}
|
this.sendInternal(
|
{
|
type: 'location',
|
longitude,
|
latitude,
|
userId,
|
timestamp: Date.now() * 1000,
|
},
|
{ silentWhenDisconnected: true }
|
);
|
} catch (error) {
|
// 定时定位失败静默处理,避免干扰业务流程
|
}
|
}
|
|
/**
|
* 重置心跳健康状态。
|
*/
|
private resetHeartbeatState() {
|
this.pendingHeartbeatSeq = null;
|
this.missedHeartbeatCount = 0;
|
if (this.heartbeatTimeoutTimer) {
|
clearTimeout(this.heartbeatTimeoutTimer);
|
this.heartbeatTimeoutTimer = null;
|
}
|
}
|
|
/**
|
* 调度下一次重连,后台期间延后到前台再处理。
|
*/
|
private scheduleReconnect(connectionId: number, reason: string) {
|
if (!this.isCurrentConnection(connectionId) || this.isManualClose || !this.url) {
|
return;
|
}
|
if (!this.isAppActive) {
|
this.shouldReconnectWhenActive = true;
|
return;
|
}
|
if (this.reconnectTimer) {
|
return;
|
}
|
|
const delay = this.getReconnectDelay();
|
console.log(`正在尝试重连,原因:${reason},延迟 ${delay}ms... (第 ${this.reconnectAttempts + 1} 次尝试)`);
|
|
this.reconnectTimer = setTimeout(() => {
|
this.reconnectTimer = null;
|
if (this.isManualClose || !this.isAppActive || !this.url) {
|
return;
|
}
|
this.reconnectAttempts += 1;
|
this.connect(this.url);
|
}, delay);
|
}
|
|
/**
|
* 获取退避后的重连延迟。
|
*/
|
private getReconnectDelay() {
|
const retryLevel = Math.min(this.reconnectAttempts, 4);
|
const baseDelay = Math.min(1000 * Math.pow(2, retryLevel), 15000);
|
const jitter = Math.floor(Math.random() * 500);
|
return baseDelay + jitter;
|
}
|
|
/**
|
* 清理已有重连任务。
|
*/
|
private clearReconnectTimer() {
|
if (this.reconnectTimer) {
|
clearTimeout(this.reconnectTimer);
|
this.reconnectTimer = null;
|
}
|
}
|
|
/**
|
* 发送消息的底层实现。
|
*/
|
private sendInternal(data: any, options: { silentWhenDisconnected: boolean }) {
|
if (!this.isConnected || this.socketState !== SocketState.CONNECTED) {
|
if (!options.silentWhenDisconnected) {
|
console.warn('WebSocket 未连接,消息未发送:', data);
|
}
|
return false;
|
}
|
|
const message = typeof data === 'string' ? data : JSON.stringify(data);
|
if (this.socketTask) {
|
this.socketTask.send({
|
data: message,
|
fail: (err) => {
|
console.error('发送消息失败:', err);
|
}
|
});
|
return true;
|
}
|
|
Taro.sendSocketMessage({
|
data: message,
|
fail: (err) => {
|
console.error('发送消息失败:', err);
|
}
|
});
|
return true;
|
}
|
|
/**
|
* 关闭指定 task,不影响当前代次判断。
|
*/
|
private closeTaskSilently(task: Taro.SocketTask | null | undefined, reason: string) {
|
if (!task) {
|
return;
|
}
|
try {
|
task.close({
|
code: 1000,
|
reason,
|
});
|
} catch (error) {
|
console.error('关闭 SocketTask 失败:', error);
|
}
|
}
|
|
/**
|
* 统一更新关闭态。
|
*/
|
private markConnectionClosed(connectionId: number) {
|
if (this.isCurrentConnection(connectionId)) {
|
this.socketTask = null;
|
}
|
this.isConnected = false;
|
this.isConnecting = false;
|
this.socketState = SocketState.IDLE;
|
this.stopHeartbeat();
|
this.stopLocationReport();
|
}
|
|
/**
|
* 通知监听器。
|
* @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) {
|
const rawType = String(data?.type || '');
|
const normalizedType = rawType.toLowerCase();
|
if (normalizedType === 'auth_success' && rawType !== 'auth_success') {
|
this.notifyListeners('auth_success', data);
|
}
|
if (data?.type === 'ORDER_STATUS') {
|
const status = content?.status;
|
const orderId = content?.orderId ?? data?.orderId;
|
if (typeof status === 'number') {
|
const mapped = this.mapOrderStatusToTripStatus(status);
|
if (typeof mapped.tripStatus === 'number') {
|
Taro.setStorageSync('tripStatus', mapped.tripStatus);
|
if (orderId) {
|
Taro.setStorageSync('orderId', orderId);
|
}
|
}
|
this.notifyListeners('order_status', { status, orderId, ...mapped, data, content });
|
}
|
}
|
if (data?.type === 'ORDER_ROUTE' || data?.type === 'ORDER_UPDATE_LINE') {
|
const routeType = content?.routeType;
|
const routeObj = safeParse(content?.route ?? '{}', {});
|
const rawDirection = content?.direction;
|
const direction = rawDirection == null ? NaN : (Number.isFinite(Number(rawDirection)) ? ((Number(rawDirection) % 360) + 360) % 360 : NaN);
|
const duration = Number(routeObj?.duration ?? '0');
|
const distance = Number(routeObj?.distance ?? '0');
|
const points = this.normalizeRoutePoints(content);
|
const servedPoints = this.normalizeServedPoints(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,
|
servedPoints,
|
data,
|
content,
|
forceFullRedraw: data?.type === 'ORDER_UPDATE_LINE'
|
});
|
}
|
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 === 'ORDER_ERROR_REASSIGN') {
|
const orderId = content?.orderId ?? data?.orderId;
|
this.notifyListeners('order_error_reassign', { orderId, data, content });
|
}
|
if (data?.type === 'OPEN_TRUNK') {
|
const isOpen = content?.isOpen ?? content?.open;
|
this.notifyListeners('open_trunk', { isOpen, data, content });
|
}
|
if (data?.type === 'USER_OPEN_DOOR') {
|
const orderId = content?.orderId ?? data?.orderId;
|
this.notifyListeners('user_open_door', { orderId, data, content });
|
}
|
if (data?.type === 'UPDATE_END_ADDRESS_STATUS') {
|
const status = Number(content?.status);
|
this.notifyListeners('update_end_address_status', { status, data, content });
|
}
|
}
|
|
/**
|
* 归一化路线点。
|
*/
|
private normalizeRoutePoints(content: any) {
|
return this.normalizeRoutePointsByKey(content, 'polyline');
|
}
|
|
/**
|
* 归一化已行驶路线点。
|
*/
|
private normalizeServedPoints(content: any) {
|
return this.normalizeRoutePointsByKey(content, 'served');
|
}
|
|
/**
|
* 根据指定 key 归一化路线点。
|
*/
|
private normalizeRoutePointsByKey(content: any, key: 'polyline' | 'served') {
|
const route = safeParse(content?.route ?? '{}', {});
|
const rawPoints = route?.[key] ?? [];
|
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: 6 };
|
case 8:
|
return { tripStatus: 8 };
|
case 9:
|
case 10:
|
return { tripStatus: 9 };
|
default:
|
return { tripStatus: undefined };
|
}
|
}
|
}
|
|
export default WebSocketManager.getInstance();
|