hejianhao
2026-04-24 62e0e9088efec67345541885aaa4f39db183e445
src/utils/websocket.ts
@@ -1,6 +1,7 @@
import Taro from '@tarojs/taro';
import { getOrCreateLocalId, getStorage } from './tools';
import { getStorage } from './tools';
import { _UID } from '../constants/api';
import { safeParse } from './Robotaxi-public-method';
type MessageCallback = (data: any) => void;
@@ -9,13 +10,22 @@
  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 heartbeatInterval: number = 30000; // 30秒心跳
  private heartbeatTimeoutTimer: any = null; // 用于检测心跳超时的定时器
  private heartbeatInterval: number = 10000; // 10秒心跳
  private heartbeatTimeout: number = 3000;   // 3秒收不到响应即视为掉线
  private listeners: Map<string, MessageCallback[]> = new Map();
  private globalEventsBound: boolean = false;
  private isHeartbeatMessage(data: any) {
    const type = String(data?.type || '').toLowerCase();
    return type === 'ping' || type === 'pong';
  }
  private constructor() { }
@@ -31,15 +41,17 @@
   * @param url WebSocket地址
   */
  public async connect(url: string) {
    if (this.socketTask || this.isConnected) {
    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) || await getOrCreateLocalId();
    const uid = await getStorage(_UID) || '-1';
    const task = Taro.connectSocket({
      header: {
        'XP-Uid': uid.toString(),
@@ -48,12 +60,14 @@
      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;
    }
@@ -61,15 +75,18 @@
      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();
  }
@@ -85,35 +102,47 @@
      this.socketTask.onOpen(async () => {
        console.log('WebSocket 连接已打开');
        this.isConnected = true;
        this.isConnecting = false;
        this.reconnectAttempts = 0;
        this.startHeartbeat();
        const uid = await getStorage(_UID) || await getOrCreateLocalId();
        this.send({ type: 'auth', appUserId: uid, timestamp: Date.now() });
        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 {
          const data = JSON.parse(res.data as string);
          const content = JSON.parse(data.content || '{}');
          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.warn('解析 WebSocket 消息失败:', res.data);
          this.notifyListeners('message', res.data);
          console.error('处理 WebSocket 消息出错:', e);
        }
      });
      this.socketTask.onClose((res) => {
        console.log('WebSocket 连接已关闭', res);
        this.isConnected = false;
        this.isConnecting = false;
        this.stopHeartbeat();
        this.socketTask = null;
        if (res.code !== 1000) {
        if (!this.isManualClose) {
          this.handleReconnect();
        }
        this.notifyListeners('close', res);
@@ -122,6 +151,12 @@
      this.socketTask.onError((err) => {
        console.error('WebSocket 错误:', err);
        this.isConnected = false;
        this.isConnecting = false;
        this.socketTask = null;
        this.stopHeartbeat();
        if (!this.isManualClose) {
          this.handleReconnect();
        }
        this.notifyListeners('error', err);
      });
      return;
@@ -133,35 +168,47 @@
    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: '12345678', timestamp: Date.now() });
      this.send({ type: 'auth', appUserId: uid.toString(), timestamp: Date.now() });
      this.notifyListeners('open', { status: 'connected' });
    });
    Taro.onSocketMessage((res) => {
      this.resetHeartbeatTimeout();
      let data: any = {};
      try {
        const data = JSON.parse(res.data as string);
        const content = JSON.parse(data.content || '{}');
        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.warn('解析 WebSocket 消息失败:', res.data);
        this.notifyListeners('message', res.data);
        console.error('处理 WebSocket 消息出错:', e);
      }
    });
    Taro.onSocketClose((res) => {
      console.log('WebSocket 连接已关闭', res);
      this.isConnected = false;
      this.isConnecting = false;
      this.stopHeartbeat();
      this.socketTask = null;
      if (res.code !== 1000) {
      if (!this.isManualClose) {
        this.handleReconnect();
      }
      this.notifyListeners('close', res);
@@ -170,6 +217,12 @@
    Taro.onSocketError((err) => {
      console.error('WebSocket 错误:', err);
      this.isConnected = false;
      this.isConnecting = false;
      this.socketTask = null;
      this.stopHeartbeat();
      if (!this.isManualClose) {
        this.handleReconnect();
      }
      this.notifyListeners('error', err);
    });
  }
@@ -206,6 +259,7 @@
   * 关闭连接
   */
  public close() {
    this.isManualClose = true;
    this.stopHeartbeat();
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
@@ -220,24 +274,24 @@
      this.socketTask = null;
    }
    this.isConnected = false;
    this.isConnecting = false;
  }
  /**
   * 处理重连
   */
  private handleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('达到最大重连次数,停止重连。');
      return;
    }
    if (this.reconnectTimer) return;
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); // 指数退避
    console.log(`正在尝试重连,延迟 ${delay}ms... (第 ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts} 次尝试)`);
    // 固定约 3 秒重连一次,并加上 0~1 秒的随机抖动
    const delay = 3000 + Math.floor(Math.random() * 1000);
    console.log(`正在尝试重连,延迟 ${delay}ms... (第 ${this.reconnectAttempts + 1} 次尝试)`);
    this.reconnectTimer = setTimeout(() => {
      this.reconnectAttempts++;
      // 防止数字溢出(无限重试时)
      if (this.reconnectAttempts < 9999) {
        this.reconnectAttempts++;
      }
      this.reconnectTimer = null;
      this.connect(this.url);
    }, delay);
@@ -251,6 +305,15 @@
    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);
  }
@@ -263,6 +326,46 @@
      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();
    }
  }
  /**
@@ -274,7 +377,10 @@
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)?.push(callback);
    const callbacks = this.listeners.get(event)!;
    if (!callbacks.includes(callback)) {
      callbacks.push(callback);
    }
  }
  /**
@@ -285,9 +391,10 @@
  public off(event: string, callback: MessageCallback) {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index !== -1) {
      let index = callbacks.indexOf(callback);
      while (index !== -1) {
        callbacks.splice(index, 1);
        index = callbacks.indexOf(callback);
      }
    }
  }
@@ -324,25 +431,23 @@
        this.notifyListeners('order_status', { status, ...mapped, data, content });
      }
    }
    if (data?.type === 'VEHICLE_LOCATION') {
      const longitude = content?.lon;
      const latitude = content?.lat;
      if (typeof longitude === 'number' && typeof latitude === 'number') {
        this.notifyListeners('vehicle_location', { longitude, latitude, data, content });
      }
    }
    if (data?.type === 'ORDER_ROUTE') {
    if (data?.type === 'ORDER_ROUTE' || data?.type === 'ORDER_UPDATE_LINE') {
      const routeType = content?.routeType;
      // 2-12 新增
      const duration = Number(JSON.parse(content?.route ?? '{}')?.duration ?? '0');
      const distance = Number(JSON.parse(content?.route ?? '{}')?.distance ?? '0');
      // 2-12 新增
      const routeObj = safeParse(content?.route ?? '{}', {});
      const rawDirection = Number(content?.direction);
      const direction = Number.isFinite(rawDirection) ? ((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', duration);
        Taro.setStorageSync('orderRouteDuration', Math.round(duration));
        Taro.setStorageSync('orderRouteDurationUnit', 's');
      }
      if (!Number.isNaN(distance)) {
        Taro.setStorageSync('orderRouteDistance', 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') || {};
@@ -353,24 +458,55 @@
          cached.latest = points;
        }
        Taro.setStorageSync('orderRoutePoints', cached);
        this.notifyListeners('order_route', { routeType, points, data, content });
      }
      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 === 'OPEN_TRUNK') {
      const isOpen = content?.isOpen ?? content?.open;
      this.notifyListeners('open_trunk', { isOpen, data, content });
    }
  }
  // 归一化路线点
  private normalizeRoutePoints(content: any) {
    const route = JSON.parse(content?.route ?? '{}');
    const rawPoints = route?.polyline ?? [];
    if (!Array.isArray(rawPoints)) return [];
    return rawPoints.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);
    return this.normalizeRoutePointsByKey(content, 'polyline');
  }
  private normalizeServedPoints(content: any) {
    return this.normalizeRoutePointsByKey(content, 'served');
  }
  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);
  }
  // 映射订单状态到行程状态
@@ -381,7 +517,6 @@
      case 1:
        return { tripStatus: 3, showPassengerNotice: true };
      case 2:
        return { tripStatus: 3 };
      case 3:
        return { tripStatus: 4 };
      case 4: